1use std::collections::BTreeSet;
2use std::fmt::{Display, Formatter};
3
4use pest_consume::Parser;
5
6use crate::todo::body_token::TodoBodyToken;
7use crate::todo::parser::TodoParserError;
8use crate::todo::parser::{Rule, TodoParser};
9use crate::TODO_DATE_FORMAT;
10
11mod body_token;
12mod builder;
13mod parser;
14
15#[derive(Eq, PartialEq, Debug)]
16pub struct Todo {
17 pub is_completed: bool,
19 pub priority: Option<char>,
20 pub creation_date: Option<time::Date>,
21 pub completion_date: Option<time::Date>,
22 body_tokens: Vec<TodoBodyToken>,
23 pub threshold_date: Option<time::Date>,
24 pub due_date: Option<time::Date>,
25 pub contexts: BTreeSet<String>,
26 pub projects: BTreeSet<String>,
27 pub is_hidden: bool,
28 }
34
35#[allow(dead_code)]
36impl Todo {
37 pub fn has_context(&self, context: &str) -> bool {
38 if !context.starts_with('@') {
39 let c = "@".to_string() + context;
40 return self.has_context(&c);
41 }
42 self.contexts.contains(context)
43 }
44
45 pub fn has_project(&self, project: &str) -> bool {
46 if !project.starts_with('+') {
47 let c = "+".to_string() + project;
48 return self.has_project(&c);
49 }
50 self.projects.contains(project)
51 }
52
53 pub fn canonical_context(&self) -> Option<String> {
54 self.contexts.first().map(|s| s.to_owned())
55 }
56
57 pub fn canonical_project(&self) -> Option<String> {
58 self.projects.first().map(|s| s.to_owned())
59 }
60
61 pub fn parse(input: &str) -> std::result::Result<Todo, TodoParserError> {
62 let input = input.trim();
63 if input.is_empty() {
64 return Err(TodoParserError::EmptyInput);
65 }
66 let nodes = TodoParser::parse(Rule::todo, input)?;
67 let node = nodes.single()?;
68 let todo = TodoParser::todo(node)?;
69 Ok(todo)
70 }
71}
72
73impl Display for Todo {
74 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
75 let mut tokens = Vec::new();
76 if self.is_completed {
77 tokens.push("x".to_string())
78 };
79 if let Some(d) = self.completion_date {
80 tokens.push(d.format(TODO_DATE_FORMAT))
81 };
82 if let Some(p) = self.priority {
83 tokens.push(format!("({})", p.to_uppercase()))
84 };
85 if let Some(d) = self.creation_date {
86 tokens.push(d.format(TODO_DATE_FORMAT))
87 };
88 for t in &self.body_tokens {
89 tokens.push(format!("{}", t))
90 }
91 write!(f, "{}", tokens.join(" "))?;
92 Ok(())
93 }
94}
95
96#[cfg(test)]
97mod test {
98 use pretty_assertions::assert_eq;
99
100 use super::*;
101
102 #[test]
103 fn round_tripping_a_todo_with_display() {
104 let input = "x 2021-01-03 (A) 2021-01-02 impl Display for Todos @pc +tudor due:2021-01-01 t:2021-01-01";
105 let t = Todo::parse(input).unwrap();
106 let got = format!("{}", t);
107 assert_eq!(input, got);
108 }
109}