devlog/
task.rs

1//! A task is something the user wants or needs to do.
2
3use std::fmt;
4
5/// Represents the user-assigned status of a task.
6#[derive(Debug, Copy, Clone, PartialEq, Eq)]
7pub enum TaskStatus {
8    /// The user has not yet started the task.
9    ToDo,
10
11    /// The user has started, but not completed, the task.
12    Started,
13
14    /// The user cannot complete the task due to external circumstances.
15    Blocked,
16
17    /// The user has completed the task.
18    Done,
19}
20
21impl TaskStatus {
22    /// Return a human-readable name for the task status.
23    pub fn display_name(&self) -> &str {
24        match self {
25            TaskStatus::ToDo => "To Do",
26            TaskStatus::Started => "In Progress",
27            TaskStatus::Blocked => "Blocked",
28            TaskStatus::Done => "Done",
29        }
30    }
31}
32
33/// A task the user wants or needs to do.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Task {
36    status: TaskStatus,
37    content: String,
38}
39
40impl Task {
41    /// Create a new task with the specified status and content.
42    pub fn new(status: TaskStatus, content: &str) -> Task {
43        Task {
44            status,
45            content: content.to_string(),
46        }
47    }
48
49    /// Parse a task from its string representation.
50    /// A task string always begins with one of four characters:
51    /// "*" means `ToDo`, "^" means `Started`, "+" means `Completed`,
52    /// and "-" means `Blocked`.  The rest of the string, except for trailing whitespace,
53    /// is the content of the task.  Returns `None` if the string is not a valid task.
54    pub fn from_string(s: &str) -> Option<Task> {
55        let parse_content = |s: &str| s[1..].trim().to_string();
56        if s.starts_with("*") {
57            Some(Task {
58                status: TaskStatus::ToDo,
59                content: parse_content(s),
60            })
61        } else if s.starts_with("^") {
62            Some(Task {
63                status: TaskStatus::Started,
64                content: parse_content(s),
65            })
66        } else if s.starts_with("+") {
67            Some(Task {
68                status: TaskStatus::Done,
69                content: parse_content(s),
70            })
71        } else if s.starts_with("-") {
72            Some(Task {
73                status: TaskStatus::Blocked,
74                content: parse_content(s),
75            })
76        } else {
77            None
78        }
79    }
80
81    /// Returns the status of the task.
82    pub fn status(&self) -> TaskStatus {
83        self.status
84    }
85
86    /// Returns the content of the task.
87    pub fn content(&self) -> &str {
88        &self.content
89    }
90}
91
92impl fmt::Display for Task {
93    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
94        match self.status {
95            TaskStatus::ToDo => write!(f, "* ")?,
96            TaskStatus::Started => write!(f, "^ ")?,
97            TaskStatus::Done => write!(f, "+ ")?,
98            TaskStatus::Blocked => write!(f, "- ")?,
99        };
100        write!(f, "{}", self.content)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_parse_todo() {
110        let t = Task::from_string("* INCOMPLETE").expect("Could not parse todo task");
111        assert_eq!(t.status(), TaskStatus::ToDo);
112        assert_eq!(t.content(), "INCOMPLETE");
113    }
114
115    #[test]
116    fn test_parse_started() {
117        let t = Task::from_string("^ STARTED").expect("Could not parse started task");
118        assert_eq!(t.status(), TaskStatus::Started);
119        assert_eq!(t.content(), "STARTED");
120    }
121
122    #[test]
123    fn test_parse_done() {
124        let t = Task::from_string("+ Done").expect("Could not parse done task");
125        assert_eq!(t.status(), TaskStatus::Done);
126        assert_eq!(t.content(), "Done");
127    }
128
129    #[test]
130    fn test_parse_blocked() {
131        let t = Task::from_string("- Blocked").expect("Could not parse blocked task");
132        assert_eq!(t.status(), TaskStatus::Blocked);
133        assert_eq!(t.content(), "Blocked");
134    }
135
136    #[test]
137    fn test_parse_ignore() {
138        let t = Task::from_string("Comment");
139        assert!(t.is_none());
140    }
141
142    #[test]
143    fn test_parse_ignore_leading_whitespace() {
144        let t = Task::from_string("     * COMMENT");
145        assert!(t.is_none());
146    }
147
148    #[test]
149    fn test_trim_whitespace() {
150        let t = Task::from_string("+    done      \n").expect("Could not parse task");
151        assert_eq!(t.status(), TaskStatus::Done);
152        assert_eq!(t.content(), "done");
153    }
154
155    #[test]
156    fn test_fmt_todo() {
157        let t = Task::new(TaskStatus::ToDo, "INCOMPLETE");
158        let s = format!("{}", t);
159        assert_eq!(s, "* INCOMPLETE");
160    }
161
162    #[test]
163    fn test_fmt_started() {
164        let t = Task::new(TaskStatus::Started, "STARTED");
165        let s = format!("{}", t);
166        assert_eq!(s, "^ STARTED");
167    }
168
169    #[test]
170    fn test_fmt_done() {
171        let t = Task::new(TaskStatus::Done, "DONE");
172        let s = format!("{}", t);
173        assert_eq!(s, "+ DONE");
174    }
175
176    #[test]
177    fn test_fmt_blocked() {
178        let t = Task::new(TaskStatus::Blocked, "BLOCKED");
179        let s = format!("{}", t);
180        assert_eq!(s, "- BLOCKED");
181    }
182}