Commit Diff


commit - 2b4c6992a668ec0a40bb37a7ef041bd46f8f2b11
commit + 157d7b0f7b1b8381c16b9761abcec10f6293ee6b
blob - d2e82dfcca37040e4bc3c22a04ff1468c659b780
blob + e002717c0474fd7c83aa1dfd3f960e2057da5db4
--- src/types.rs
+++ src/types.rs
@@ -6,7 +6,10 @@
 
 use crate::err::TodoError;
 use crate::parser;
-use chrono::naive::NaiveDate;
+use chrono::{
+    naive::{Days, NaiveDate},
+    Months,
+};
 use nom::Finish;
 use std::{borrow::Cow, num::NonZeroU8};
 
@@ -421,13 +424,65 @@ impl<'a> Task<'a> {
         }
     }
 
-    /// Return the recurring rule of the task, if there is one.
+    /// Return the recurring rule of the [`Task`], if there is one.
     #[must_use]
     pub fn rec(&self) -> Option<Recurring> {
         self.tags()
             .into_iter()
             .find_map(|t| if let Tag::Rec(r) = t { Some(r) } else { None })
     }
+
+    /// Set the [`Recurring`] rule of the [`Task`], creating a new one.
+    ///
+    /// If the [`Task`] does not yet have a [`Recurring`] rule, it is being set by appending the
+    /// `rec` tag at the end of the description.
+    /// If the [`Recurring`] rule is already set, it modifies it in place, setting the new rule.
+    #[must_use]
+    pub fn set_rec(&'a self, rec: Recurring) -> Task<'a> {
+        let re = crate::regex!(r"(rec:)+?\d+[dwmy]");
+
+        if re.is_match(self.description.as_ref()) {
+            let description = re.replace(self.description.as_ref(), format!("${{1}}{rec}"));
+            Task {
+                completion: self.completion,
+                priority: self.priority,
+                creation_date: self.creation_date,
+                // `replace` must have found a match because the call to `is_match` returned true,
+                // so calling `clone` is fine
+                description: description.clone(),
+            }
+        } else {
+            self.add_tag(&Tag::Rec(rec))
+        }
+    }
+
+    /// Advance the due date of the [`Task`] by the value of the [`Recurring`] [`Tag`].
+    ///
+    /// The due date is only advanced if there is one.
+    #[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 prev_due = self.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()?),
+        };
+
+        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>
@@ -595,4 +650,43 @@ mod tests {
             vec![Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 9).unwrap())]
         );
     }
+
+    #[test]
+    fn test_next_due_with_rec() {
+        let task_without_due = Task::try_from("2023-01-08 boring task").unwrap();
+        let rec = Recurring {
+            strict: false,
+            amount: TimeAmount {
+                amount: 4,
+                unit: TimeUnit::Day,
+            },
+        };
+        assert_eq!(task_without_due.next_due_with_rec(&rec), None);
+
+        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();
+
+        assert_eq!(
+            new_task.due().unwrap(),
+            NaiveDate::from_ymd_opt(2023, 01, 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();
+
+        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()
+        );
+    }
 }