commit bc6fb9a91750c4bf7c6dc6318f8a6c6bf0e0289f from: witcher date: Sun Jan 8 20:13:05 2023 UTC Implement respecting the strict rule of `rec` The `next_due` and `next_due_with_rec` methods now respect the strict attribute of the recurring rule. To still be able to test the method, a third method, `next_due_with_rec_and_date` has been created which, instead of internally computing the current date, explicitly takes the supposedly current date. The `next_due_with_rec_and_date` method is public for now but might be made private, depending on how useful it might be. Implements: https://todo.sr.ht/~witcher/todotxt/4 commit - c25ff820f4b12628b9df9a9b35435c01da6f1606 commit + bc6fb9a91750c4bf7c6dc6318f8a6c6bf0e0289f blob - f56b69375d1a7f1da89b96ad36c235df0a6dff6f blob + a72e1d4282795ab9a62b1b4a888d4ad1c2ec93ab --- src/types.rs +++ src/types.rs @@ -459,30 +459,63 @@ impl<'a> Task<'a> { /// Advance the due date of the [`Task`] by the value of the [`Recurring`] [`Tag`]. /// /// The due date is only advanced if there is one. + /// Depending on whether the [`Recurring`] rule is strict or not, the next due date will be + /// based on the current date or the date given in the due [`Tag`], respectively. + /// + /// The current date is calculated internally. If a different current date is to be given, use + /// [`next_due_with_rec_and_date`] instead. #[must_use] pub fn next_due_with_rec(&self, rec: &Recurring) -> Option { - // TODO: set new creation date, too - // TODO: handle strict recurring - // non-strict: next due date is x amount after due date - // strict: next due date is x amount after *current* date + let current_date = chrono::offset::Utc::now().date_naive(); + + self.next_due_with_rec_and_date(rec, current_date) + } + + /// Advance the due date of the [`Task`] by the value of the [`Recurring`] [`Tag`]. + /// + /// The due date is only advanced if both a due date *and* a [`Recurring`] [`Tag`] are present. + /// Depending on whether the [`Recurring`] rule is strict or not, the next due date will be + /// based on the current date or the date given in the due [`Tag`], respectively. + /// + /// The current date is calculated internally. If a different current date is to be given, use + /// [`next_due_with_rec_and_date`] instead. + #[must_use] + pub fn next_due(&self) -> Option { + self.next_due_with_rec(&self.rec()?) + } + + /// Advance the due date of the [`Task`] by the value of the [`Recurring`] [`Tag`], given an + /// explicit current date instead of computing one internally. + /// + /// The due date is only advanced if both a due date *and* a [`Recurring`] [`Tag`] are present. + /// Depending on whether the [`Recurring`] rule is strict or not, the next due date will be + /// based on the current date or the date given in the due [`Tag`], respectively. + // mostly an internal function of `next_due` and `next_due_with_rec` for easier testing that is + // not relying on an implicit "today" date. only public for the chance it *might* be useful. + #[must_use] + pub fn next_due_with_rec_and_date( + &self, + rec: &Recurring, + current_date: NaiveDate, + ) -> Option { + // abort early if no previous due date was set + // XXX: if performance is needed *badly*, optimise to "lazily" compute the due date when it + // is needed let prev_due = self.due()?; + + let reference_date = if rec.strict { current_date } else { prev_due }; + let due = match rec.amount.unit { - TimeUnit::Day => prev_due + Days::new(rec.amount.amount.try_into().ok()?), - TimeUnit::Week => prev_due + Days::new((rec.amount.amount * 7).try_into().ok()?), - TimeUnit::Month => prev_due + Months::new(rec.amount.amount.try_into().ok()?), - TimeUnit::Year => prev_due + Months::new((rec.amount.amount * 12).try_into().ok()?), + TimeUnit::Day => reference_date + Days::new(rec.amount.amount.try_into().ok()?), + TimeUnit::Week => reference_date + Days::new((rec.amount.amount * 7).try_into().ok()?), + TimeUnit::Month => reference_date + Months::new(rec.amount.amount.try_into().ok()?), + TimeUnit::Year => { + reference_date + Months::new((rec.amount.amount * 12).try_into().ok()?) + } }; Some(self.set_due(due)) } - - /// Advance the due date of the [`Task`] by the value of the [`Recurring`] [`Tag`]. - /// - /// The due date is only advanced if both a due date *and* a [`Recurring`] [`Tag`] are present. - #[must_use] - pub fn next_due(&self) -> Option { - self.next_due_with_rec(&self.rec()?) - } } impl<'a> @@ -632,7 +665,7 @@ mod tests { fn test_set_due() { let task = Task::try_from("test with due date due:2023-01-08").unwrap(); let due = NaiveDate::from_ymd_opt(2023, 1, 9).unwrap(); - let new_task = task.set_due(due.clone()); + let new_task = task.set_due(due); assert_eq!( task.tags(), vec![Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 8).unwrap())] @@ -652,41 +685,49 @@ mod tests { } #[test] - fn test_next_due_with_rec() { - let task_without_due = Task::try_from("2023-01-08 boring task").unwrap(); - let rec = Recurring { + fn test_next_due_with_rec_and_date() { + let current_date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap(); + let rec_not_strict = Recurring { strict: false, amount: TimeAmount { amount: 4, unit: TimeUnit::Day, }, }; - assert_eq!(task_without_due.next_due_with_rec(&rec), None); + let rec_strict = Recurring { + strict: true, + amount: TimeAmount { + amount: 4, + unit: TimeUnit::Day, + }, + }; - let task_with_due = Task::try_from("2023-01-08 due task due:2023-01-09").unwrap(); - let new_task = task_with_due.next_due_with_rec(&rec).unwrap(); + // no due date given -> no new task + let task_without_due = Task::try_from("2023-01-06 boring task").unwrap(); + assert_eq!( + task_without_due.next_due_with_rec_and_date(&rec_not_strict, current_date), + None + ); + // not strict -> due + rec, current date is irrelevant + let task_with_due = Task::try_from("2023-01-06 due task due:2023-01-09").unwrap(); + let new_task = task_with_due + .next_due_with_rec_and_date(&rec_not_strict, current_date) + .unwrap(); + assert_eq!( new_task.due().unwrap(), - NaiveDate::from_ymd_opt(2023, 01, 13).unwrap() + NaiveDate::from_ymd_opt(2023, 1, 13).unwrap() ); - } - #[test] - fn test_next_due() { - let task_without_due_without_rec = Task::try_from("2023-01-08 boring task").unwrap(); - let task_without_due_with_rec = Task::try_from("2023-01-08 boring task rec:+4d").unwrap(); - let task_with_due_without_rec = - Task::try_from("2023-01-08 boring task due:2023-01-09").unwrap(); - let task_with_due_with_rec = - Task::try_from("2023-01-08 boring task due:2023-01-09 rec:+4d").unwrap(); + // strict -> current_date + rec, due is irrelevant + let new_strict_task = task_with_due + .next_due_with_rec_and_date(&rec_strict, current_date) + .unwrap(); - assert_eq!(task_without_due_without_rec.next_due(), None); - assert_eq!(task_without_due_with_rec.next_due(), None); - assert_eq!(task_with_due_without_rec.next_due(), None); assert_eq!( - task_with_due_with_rec.next_due().unwrap(), - Task::try_from("2023-01-08 boring task due:2023-01-13 rec:+4d").unwrap() + new_strict_task.due().unwrap(), + NaiveDate::from_ymd_opt(2023, 1, 12).unwrap() ); } }