commit 4846caa86f50134b6790df48298060c52944a501 from: witcher date: Sun Jan 8 11:12:06 2023 UTC Add getter/setter to `Task` The attributes of the `Task` struct will remain private and access is granted through getter and setter. Additionally, methods for adding tags have been added. commit - 82d62f1006b9df033509bb03d9cf3b550fe2d7bc commit + 4846caa86f50134b6790df48298060c52944a501 blob - 7ef625469dadf66107a9d7721f134eac58a93593 blob + a6008ec414cdc9cce23379b272b25193e1ae49ed --- Cargo.toml +++ Cargo.toml @@ -19,4 +19,6 @@ publish = false # set to true when publishing [dependencies] chrono = "0.4.23" nom = "7.1.2" +once_cell = "1.17.0" +regex = "1.7.0" thiserror = "1.0.38" blob - cc4598b14dc65e776e011b82629b41812d796dd5 blob + 69b270b3a5c49ee1c5de1a076b3dc9dfc66b62b8 --- src/types.rs +++ src/types.rs @@ -206,14 +206,14 @@ impl TryFrom<(bool, Option)> for Completion #[derive(Debug, PartialEq, Eq)] pub struct Task<'a> { /// Whether the [`Task`] has been completed or not. - pub completion: Completion, + completion: Completion, /// What priority the [`Task`] is associated with. /// /// A priority of 0 indicates no priority. Possible priority values are in range `1..=26`, /// corresponding to letters in range `A..=Z`. - pub priority: u8, + priority: u8, /// Date of creation. - pub creation_date: Option, + creation_date: Option, /// Description of the [`Task`]. description: Cow<'a, str>, } @@ -221,15 +221,19 @@ pub struct Task<'a> { impl<'a> Task<'a> { /// Create a new task. /// - /// The tags of the task are parsed from the description. As the tags and the description are - /// closely tied together, one changing if the other does, mutable access is only granted to - /// the description, which, when changed, changes the tags, too. + /// The tags of the task are parsed on demand from the description. /// /// ```rust /// # use todotxt_parser::{Completion, Tag, Task, TodoError}; - /// # use chrono::NaiveDate; - /// let raw_task = "x (A) 2016-05-20 2016-04-30 measure space for +chapelShelving @chapel due:2016-05-30"; - /// let task = Task::try_from(raw_task)?; + /// use chrono::NaiveDate; + /// + /// let task = Task::new( + /// Completion::Complete(Some(NaiveDate::from_ymd_opt(2016, 5, 20).unwrap())), + /// 1, + /// Some(NaiveDate::from_ymd_opt(2016, 4, 30).unwrap()), + /// "measure space for +chapelShelving @chapel due:2016-05-30" + /// ); + /// /// assert_eq!(task.tags(), vec![ /// Tag::Project("chapelShelving"), /// Tag::Context("chapel"), @@ -252,35 +256,75 @@ impl<'a> Task<'a> { } } + /// Return the completion status. #[must_use] - pub const fn has_changed(&self) -> bool { - matches!(self.description, Cow::Owned(_)) - } - - #[must_use] pub const fn completion(&self) -> &Completion { &self.completion } + /// Set the completion status, returning a new [`Task`]. #[must_use] + pub fn set_completion(&self, completion: Completion) -> Task<'a> { + Task { + completion, + priority: self.priority, + creation_date: self.creation_date, + description: self.description.clone(), + } + } + + /// Return the priority. + #[must_use] pub const fn priority(&self) -> u8 { self.priority } + /// Set the priority, returning a new [`Task`]. #[must_use] + pub fn set_priority(&self, priority: u8) -> Task<'a> { + Task { + completion: self.completion, + priority, + creation_date: self.creation_date, + description: self.description.clone(), + } + } + + /// Return the creation date. + #[must_use] pub const fn creation_date(&self) -> Option { self.creation_date } + /// Set the creation date, returning a new [`Task`]. #[must_use] + pub fn set_creation_date(&self, creation_date: Option) -> Task<'a> { + Task { + completion: self.completion, + priority: self.priority, + creation_date, + description: self.description.clone(), + } + } + + /// Return the description. + #[must_use] pub fn description(&'a self) -> &'a str { self.description.as_ref() } - pub fn set_description(&mut self, description: String) { - self.description = Cow::from(description); + /// Set the description, returning a new [`Task`]. + #[must_use] + pub fn set_description(&self, description: String) -> Task<'a> { + Task { + completion: self.completion, + priority: self.priority, + creation_date: self.creation_date, + description: Cow::from(description), + } } + /// Return the set tags. #[must_use] pub fn tags(&self) -> Vec { parser::description_tags(self.description.as_ref()) @@ -288,6 +332,30 @@ impl<'a> Task<'a> { .unwrap_or_default() } + /// Add a new tag to the [`Task`], creating a new one. + /// + /// The new tag will be added at the end of the description, prefixed with a space if the + /// description is not empty and doesn't end with a space. + #[must_use] + pub fn add_tag(&self, tag: &Tag) -> Task<'a> { + let mut description = self.description.clone(); + + let new_tag = if !description.is_empty() && !description.ends_with(' ') { + String::from(" ") + } else { + String::new() + } + &tag.to_string(); + + description.to_mut().push_str(&new_tag); + + Task { + completion: self.completion, + priority: self.priority, + creation_date: self.creation_date, + description, + } + } + /// Return the projects found in the description of the task. #[must_use] pub fn projects(&'a self) -> Vec<&'a str> { @@ -326,6 +394,33 @@ impl<'a> Task<'a> { .find_map(|t| if let Tag::Due(d) = t { Some(d) } else { None }) } + /// Set the due date of the [`Task`], creating a new one. + /// + /// If the [`Task`] does not yet have a due date, it is being set by appending the `due` tag at + /// the end of the description. + /// If the date is already set, it modifies it in place, setting the new date. + #[must_use] + pub fn set_due(&'a self, due: NaiveDate) -> Task<'a> { + let re = crate::regex!(r"(due:)\d{4}-\d{2}-\d{2}"); + + if re.is_match(self.description.as_ref()) { + let description = re.replace( + self.description.as_ref(), + format!("${{1}}{}", due.format("%Y-%m-%d")), + ); + 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::Due(due)) + } + } + /// Return the recurring rule of the task, if there is one. #[must_use] pub fn rec(&self) -> Option { @@ -401,8 +496,16 @@ impl<'a> TryFrom<&'a str> for Task<'a> { } } +#[macro_export] +macro_rules! regex { + ($re:literal $(,)?) => {{ + static RE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); + RE.get_or_init(|| regex::Regex::new($re).unwrap()) + }}; +} + #[cfg(test)] -mod test { +mod tests { use super::*; #[test] @@ -450,4 +553,46 @@ mod test { assert_eq!(task.rec(), Some(rec)); } + + #[test] + fn test_add_tag() { + let task_without_space = Task::try_from("task without space at the end").unwrap(); + let new_tag_task = task_without_space.add_tag(&Tag::Project("project")); + assert_eq!( + new_tag_task.description(), + "task without space at the end +project" + ); + assert_eq!(new_tag_task.tags(), vec![Tag::Project("project")],); + + let task_with_space = Task::try_from("task with space at the end ").unwrap(); + let new_space_tag_task = task_with_space.add_tag(&Tag::Project("project")); + assert_eq!( + new_space_tag_task.description(), + "task with space at the end +project" + ); + assert_eq!(new_space_tag_task.tags(), vec![Tag::Project("project")]); + } + + #[test] + 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()); + assert_eq!( + task.tags(), + vec![Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 8).unwrap())] + ); + assert_eq!( + new_task.tags(), + vec![Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 9).unwrap())] + ); + + let no_due_task = Task::try_from("test without due date").unwrap(); + let new_due_task = no_due_task.set_due(due); + assert_eq!(no_due_task.tags(), vec![]); + assert_eq!( + new_due_task.tags(), + vec![Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 9).unwrap())] + ); + } }