Commit Diff


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<Task> {
-        // 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<Task> {
+        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<Task> {
+        // 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<Task> {
-        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()
         );
     }
 }