commit - 2b4c6992a668ec0a40bb37a7ef041bd46f8f2b11
commit + 157d7b0f7b1b8381c16b9761abcec10f6293ee6b
blob - d2e82dfcca37040e4bc3c22a04ff1468c659b780
blob + e002717c0474fd7c83aa1dfd3f960e2057da5db4
--- src/types.rs
+++ src/types.rs
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};
}
}
- /// 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>
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()
+ );
+ }
}