1mod parse_today;
2mod parser_due_date;
3mod parser_priorities;
4mod parser_state;
5mod parser_tags;
6mod parser_time;
7mod token;
8
9use chrono::NaiveDateTime;
10use parse_today::parse_today;
11use parser_due_date::parse_naive_date;
12use parser_priorities::parse_priority;
13use parser_state::parse_task_state;
14use parser_tags::parse_tag;
15use parser_time::parse_naive_time;
16use token::Token;
17use tracing::error;
18use winnow::{
19 combinator::{alt, fail, repeat},
20 token::any,
21 PResult, Parser,
22};
23
24use crate::{
25 task::{DueDate, Task},
26 TasksConfig,
27};
28
29fn parse_token(input: &mut &str, config: &TasksConfig) -> PResult<Token> {
31 alt((
32 |input: &mut &str| parse_naive_date(input, config.use_american_format),
33 parse_naive_time,
34 parse_tag,
35 |input: &mut &str| parse_task_state(input, &config.task_state_markers),
36 parse_priority,
37 parse_today,
38 |input: &mut &str| {
39 let res = repeat(0.., any)
40 .fold(String::new, |mut string, c| {
41 string.push(c);
42 string
43 })
44 .parse_next(input)?;
45 Ok(Token::Name(res))
46 },
47 ))
48 .parse_next(input)
49}
50
51#[allow(clippy::module_name_repetitions)]
57pub fn parse_task(input: &mut &str, filename: String, config: &TasksConfig) -> PResult<Task> {
58 let task_state = match parse_task_state(input, &config.task_state_markers)? {
59 Token::State(state) => Ok(state),
60 _ => fail(input),
61 }?;
62
63 let mut token_parser = |input: &mut &str| parse_token(input, config);
64
65 let tokens = input
66 .split_ascii_whitespace()
67 .map(|token| token_parser.parse(token));
68
69 let mut task = Task {
70 state: task_state,
71 filename,
72 ..Default::default()
73 };
74
75 let mut due_date_opt = None;
77 let mut due_time_opt = None;
78 let mut name_vec = vec![]; for token_res in tokens {
81 match token_res {
82 Ok(Token::DueDate(date)) => due_date_opt = Some(date),
83 Ok(Token::DueTime(time)) => due_time_opt = Some(time),
84 Ok(Token::Name(name)) => name_vec.push(name),
85 Ok(Token::Priority(p)) => task.priority = p,
86 Ok(Token::State(state)) => task.state = state,
87 Ok(Token::Tag(tag)) => {
88 if let Some(ref mut tags) = task.tags {
89 tags.push(tag);
90 } else {
91 task.tags = Some(vec![tag]);
92 }
93 }
94 Ok(Token::TodayFlag) => task.is_today = true,
95 Err(error) => error!("Error: {error:?}"),
96 }
97 }
98
99 if !name_vec.is_empty() {
100 task.name = name_vec.join(" ");
101 }
102
103 let now = chrono::Local::now();
104 let (due_date, has_date) = (
105 due_date_opt.unwrap_or_else(|| now.date_naive()),
106 due_date_opt.is_some(),
107 );
108 let (due_time, has_time) = (
109 due_time_opt.unwrap_or_else(|| now.time()),
110 due_time_opt.is_some(),
111 );
112 let due_date_time = if has_date {
113 if has_time {
114 DueDate::DayTime(NaiveDateTime::new(due_date, due_time))
115 } else {
116 DueDate::Day(due_date)
117 }
118 } else if has_time {
119 DueDate::DayTime(NaiveDateTime::new(now.date_naive(), due_time))
120 } else {
121 DueDate::NoDate
122 };
123 task.due_date = due_date_time;
124 Ok(task)
125}
126#[cfg(test)]
127mod test {
128
129 use chrono::{Datelike, Days, NaiveDate, NaiveDateTime, NaiveTime};
130
131 use crate::{
132 parser::task::parse_task,
133 task::{DueDate, State, Task},
134 TasksConfig,
135 };
136 #[test]
137 fn test_parse_task_no_description() {
138 let mut input = "- [x] 10/15 task_name #done";
139 let config = TasksConfig {
140 use_american_format: true,
141 ..Default::default()
142 };
143 let res = parse_task(&mut input, String::new(), &config);
144 assert!(res.is_ok());
145 let res = res.unwrap();
146 let year = chrono::Local::now().year();
147 let expected = Task {
148 name: "task_name".to_string(),
149 description: None,
150 tags: Some(vec!["done".to_string()]),
151 due_date: DueDate::Day(NaiveDate::from_ymd_opt(year, 10, 15).unwrap()),
152 priority: 0,
153 state: State::Done,
154 line_number: 1,
155 ..Default::default()
156 };
157 assert_eq!(res, expected);
158 }
159
160 #[test]
161 fn test_parse_task_only_state() {
162 let mut input = "- [ ]";
163 let config = TasksConfig::default();
164 let res = parse_task(&mut input, String::new(), &config);
165 assert!(res.is_ok());
166 let res = res.unwrap();
167 let expected = Task {
168 subtasks: vec![],
169 name: String::new(),
170 description: None,
171 tags: None,
172 due_date: DueDate::NoDate,
173 priority: 0,
174 state: State::ToDo,
175 line_number: 1,
176 filename: String::new(),
177 is_today: false,
178 };
179 assert_eq!(res, expected);
180 }
181 #[test]
182 fn test_parse_task_with_due_date_words() {
183 let mut input = "- [ ] today 15:30 task_name";
184 let config = TasksConfig::default();
185 let res = parse_task(&mut input, String::new(), &config);
186 assert!(res.is_ok());
187 let res = res.unwrap();
188 let expected_date = chrono::Local::now().date_naive();
189 let expected_time = NaiveTime::from_hms_opt(15, 30, 0).unwrap();
190 let expected_due_date = DueDate::DayTime(NaiveDateTime::new(expected_date, expected_time));
191 assert_eq!(res.due_date, expected_due_date);
192 }
193
194 #[test]
195 fn test_parse_task_with_weekday() {
196 let mut input = "- [ ] monday 15:30 task_name";
197 let config = TasksConfig::default();
198 let res = parse_task(&mut input, String::new(), &config);
199 assert!(res.is_ok());
200 let res = res.unwrap();
201
202 let now = chrono::Local::now();
203 let expected_date = now
204 .date_naive()
205 .checked_add_days(Days::new(
206 8 - u64::from(now.date_naive().weekday().number_from_monday()),
207 ))
208 .unwrap();
209 let expected_time = NaiveTime::from_hms_opt(15, 30, 0).unwrap();
210 let expected_due_date = DueDate::DayTime(NaiveDateTime::new(expected_date, expected_time));
211 assert_eq!(res.due_date, expected_due_date);
212 }
213
214 #[test]
215 fn test_parse_task_with_weekday_this() {
216 let mut input = "- [ ] this monday 15:30 task_name";
217 let config = TasksConfig::default();
218 let res = parse_task(&mut input, String::new(), &config);
219 assert!(res.is_ok());
220 let res = res.unwrap();
221 let now = chrono::Local::now();
222 let expected_date = now
223 .date_naive()
224 .checked_add_days(Days::new(
225 8 - u64::from(now.date_naive().weekday().number_from_monday()),
226 ))
227 .unwrap();
228 let expected_time = NaiveTime::from_hms_opt(15, 30, 0).unwrap();
229 let expected_due_date = DueDate::DayTime(NaiveDateTime::new(expected_date, expected_time));
230 assert_eq!(res.due_date, expected_due_date);
231 }
232
233 #[test]
234 fn test_parse_task_with_weekday_next() {
235 let mut input = "- [ ] next monday 15:30 task_name";
236 let config = TasksConfig::default();
237 let res = parse_task(&mut input, String::new(), &config);
238 assert!(res.is_ok());
239 let res = res.unwrap();
240 let now = chrono::Local::now();
241 let expected_date = now
242 .date_naive()
243 .checked_add_days(Days::new(
244 8 - u64::from(now.date_naive().weekday().number_from_monday()),
245 ))
246 .unwrap();
247 let expected_time = NaiveTime::from_hms_opt(15, 30, 0).unwrap();
248 let expected_due_date = DueDate::DayTime(NaiveDateTime::new(expected_date, expected_time));
249 assert_eq!(res.due_date, expected_due_date);
250 }
251
252 #[test]
253 fn test_parse_task_without_due_date() {
254 let mut input = "- [ ] task_name";
255 let config = TasksConfig::default();
256 let res = parse_task(&mut input, String::new(), &config);
257 assert!(res.is_ok());
258 let res = res.unwrap();
259 let expected_due_date = DueDate::NoDate;
260 assert_eq!(res.due_date, expected_due_date);
261 }
262
263 #[test]
264 fn test_parse_task_with_invalid_state() {
265 let mut input = "- [invalid] task_name";
266 let config = TasksConfig::default();
267 let res = parse_task(&mut input, String::new(), &config);
268 assert!(res.is_err());
269 }
270
271 #[test]
272 fn test_parse_task_without_state() {
273 let mut input = "task_name";
274 let config = TasksConfig::default();
275 let res = parse_task(&mut input, String::new(), &config);
276 assert!(res.is_err());
277 }
278
279 #[test]
280 fn test_parse_task_with_invalid_priority() {
281 let mut input = "- [ ] task_name p-9";
282 let config = TasksConfig::default();
283 let res = parse_task(&mut input, String::new(), &config);
284 assert!(res.is_ok());
285 let res = res.unwrap();
286 assert_eq!(res.priority, 0);
287 }
288
289 #[test]
290 fn test_parse_task_without_name() {
291 let mut input = "- [ ]";
292 let config = TasksConfig::default();
293 let res = parse_task(&mut input, String::new(), &config);
294 assert!(res.is_ok());
295 let res = res.unwrap();
296 assert_eq!(res.name, ""); }
298 #[test]
299 fn test_parse_task_with_today_flag() {
300 let mut input = "- [ ] @t";
301 let config = TasksConfig::default();
302 let res = parse_task(&mut input, String::new(), &config);
303 assert!(res.is_ok());
304 let res = res.unwrap();
305 assert!(res.is_today);
306 }
307}