commit 01ba7a35eb3e4796a5d0cdf8a5d72c53d4a7c3e4 from: witcher date: Sat Jan 7 10:13:50 2023 UTC Remove field `tags` from struct `Task` altogether As the `tags` field is essentially just a view into the `description` field and keeping both fields synchronised can be a burden, the `tags` field has been removed and tags will now be computed on the fly, if necessary. Additionally, `description` has been changed to a `Cow` since it can change and thus needs to be able to be an owned type. It nothing is changed, however, the description will still only be a slice of the original one, avoiding multiple copies being stored in memory. If the description of the task needs to be changed, `description` will become an owned field. commit - e79ffd7b92c4807a481f6ad3d27605e484f4f58d commit + 01ba7a35eb3e4796a5d0cdf8a5d72c53d4a7c3e4 blob - 38e4d1b8f5e505c581996508721002c641872c81 blob + e6891cd04ab7707206157794d5d2314001bfaa1a --- src/lib.rs +++ src/lib.rs @@ -38,7 +38,7 @@ use nom::Finish; /// let input = "2023-01-05 sample task"; /// let res = parse_from_str(input)?; /// assert_eq!(res, vec![Task::new(Completion::Incomplete, 0, Some(NaiveDate::from_ymd_opt(2023, 1, 5).unwrap()), "sample task")]); -/// assert_eq!(res.iter().map(Task::tags).collect::>>(), vec![&vec![]]); +/// assert_eq!(res.iter().map(Task::tags).collect::>>(), vec![vec![]]); /// # Ok::<(), TodoError>(()) /// ``` /// blob - f7807f7a2f69ab415d0e9463f3b32ef1bafe4f90 blob + 430004c8b9e8287caac59bd1211e1cdfd9fb36e5 --- src/parser.rs +++ src/parser.rs @@ -533,7 +533,7 @@ mod tests { "a perfectly normal task +testing @home due:2023-01-02" ) ); - assert_eq!(t1.tags(), &tags1); + assert_eq!(t1.tags(), tags1); let (_ ,t2) = task("x 2023-01-02 2023-01-01 a completed task with completion date +testing @home due:2023-01-02 @place +todotxt-parser").unwrap(); let tags2 = vec![ @@ -547,7 +547,7 @@ mod tests { t2, Task::new(Completion::Complete(Some(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap())), 0, Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), "a completed task with completion date +testing @home due:2023-01-02 @place +todotxt-parser") ); - assert_eq!(t2.tags(), &tags2); + assert_eq!(t2.tags(), tags2); } #[test] @@ -582,8 +582,8 @@ mod tests { Task::new(Completion::Complete(Some(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap())), 0, Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), "a completed task with completion date +testing @home due:2023-01-02 @place +todotxt-parser and recurring every rec:+4m 4 months strictly") ] ); - assert_eq!(ts[0].tags(), &tags1); - assert_eq!(ts[1].tags(), &tags2); + assert_eq!(ts[0].tags(), tags1); + assert_eq!(ts[1].tags(), tags2); } #[test] blob - d0dd3c3b174c908a313e24cb9f73e95a9a0614d4 blob + 38241164113bfce4b50fd85ba3c75ae01d2f058f --- src/types.rs +++ src/types.rs @@ -8,7 +8,7 @@ use crate::err::TodoError; use crate::parser; use chrono::naive::NaiveDate; use nom::Finish; -use std::num::NonZeroU8; +use std::{borrow::Cow, num::NonZeroU8}; /// A Tag in a Task. #[non_exhaustive] @@ -172,15 +172,7 @@ pub struct Task<'a> { /// Date of creation. pub creation_date: Option, /// Description of the [`Task`]. - // `description` can't be public as it is closely tied to `tags`. Changing one means changing - // the other. Access and modification should be done through separate methods. - description: &'a str, - /// Associated [`Tag`]s. - /// - /// For a list of supported tags, see [`Tag`]. - // `tags` can't be public as it is closely tied to `description`. Changing one means changing - // the other. Access and modification should be done through separate methods. - tags: Vec>, + description: Cow<'a, str>, } impl<'a> Task<'a> { @@ -195,7 +187,7 @@ impl<'a> Task<'a> { /// # 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)?; - /// assert_eq!(task.tags(), &vec![ + /// assert_eq!(task.tags(), vec![ /// Tag::Project("chapelShelving"), /// Tag::Context("chapel"), /// Tag::Due(NaiveDate::from_ymd_opt(2016, 5, 30).unwrap()) @@ -209,72 +201,58 @@ impl<'a> Task<'a> { creation_date: Option, description: &'a str, ) -> Self { - let tags = parser::description_tags(description) - .map(|(_, res)| res) - .unwrap_or_default(); - Self { completion, priority, creation_date, - description, - tags, + description: Cow::from(description), } } #[must_use] + pub const fn has_changed(&self) -> bool { + matches!(self.description, Cow::Owned(_)) + } + + #[must_use] pub const fn completion(&self) -> &Completion { &self.completion } - pub fn set_completion(&mut self, completion: Completion) { - self.completion = completion; - } - #[must_use] pub const fn priority(&self) -> u8 { self.priority } - pub fn set_priority(&mut self, priority: u8) { - self.priority = priority; - } - #[must_use] pub const fn creation_date(&self) -> Option { self.creation_date } - pub fn set_creation_date(&mut self, creation_date: Option) { - self.creation_date = creation_date; + #[must_use] + pub fn description(&'a self) -> &'a str { + self.description.as_ref() } - #[must_use] - pub const fn description(&self) -> &'a str { - self.description + pub fn set_description(&mut self, description: String) { + self.description = Cow::from(description); } - pub fn set_description(&mut self, description: &'a str) { - let tags = parser::description_tags(description) + #[must_use] + pub fn tags(&self) -> Vec { + parser::description_tags(self.description.as_ref()) .map(|(_, res)| res) - .unwrap_or_default(); - self.description = description; - self.tags = tags; + .unwrap_or_default() } - #[must_use] - pub const fn tags(&self) -> &Vec { - &self.tags - } - /// Return the projects found in the description of the task. #[must_use] - pub fn projects(&self) -> Vec<&'a str> { - self.tags - .iter() + pub fn projects(&'a self) -> Vec<&'a str> { + self.tags() + .into_iter() .filter_map(|t| { if let Tag::Project(p) = t { - Some(*p) + Some(p) } else { None } @@ -284,12 +262,12 @@ impl<'a> Task<'a> { /// Return the contexts found in the description of the task. #[must_use] - pub fn contexts(&self) -> Vec<&'a str> { - self.tags - .iter() + pub fn contexts(&'a self) -> Vec<&'a str> { + self.tags() + .into_iter() .filter_map(|t| { if let Tag::Context(c) = t { - Some(*c) + Some(c) } else { None } @@ -299,17 +277,17 @@ impl<'a> Task<'a> { /// Return the due date of the task, if there is one. #[must_use] - pub fn due(&self) -> Option<&NaiveDate> { - self.tags - .iter() + pub fn due(&self) -> Option { + self.tags() + .into_iter() .find_map(|t| if let Tag::Due(d) = t { Some(d) } else { None }) } /// Return the recurring rule of the task, if there is one. #[must_use] - pub fn rec(&self) -> Option<&Recurring> { - self.tags - .iter() + pub fn rec(&self) -> Option { + self.tags() + .into_iter() .find_map(|t| if let Tag::Rec(r) = t { Some(r) } else { None }) } } @@ -360,17 +338,11 @@ impl<'a> } }; - let tags = parser::description_tags(description) - .finish() - .map_err(|_| TodoError::ParsingError("description tags"))? - .1; - Ok(Task { completion: (completed, completion_date).try_into()?, priority, creation_date, - description, - tags, + description: description.into(), }) } } @@ -430,10 +402,9 @@ mod test { completion: Completion::Incomplete, priority: 0, creation_date: None, - description: "sample task +project rec:1y", - tags: vec![Tag::Project("project"), Tag::Rec(rec.clone())], + description: Cow::from("sample task +project rec:1y"), }; - assert_eq!(task.rec(), Some(&rec)); + assert_eq!(task.rec(), Some(rec)); } } blob - 33bed8d1a6b05432b25add2ec1484285a2d4aa09 blob + 95c393ca1113f99f48105505fa56230299a75a51 --- tests/test.rs +++ tests/test.rs @@ -12,12 +12,11 @@ fn test_parse_from_str_oneline() { let input = "2023-01-03 write tests for todotxt-parser +todotxt-parser @workstation due:2023-01-04"; let ts = parse_from_str(&input).unwrap(); - let tags1 = vec![ + let tags = vec![vec![ Tag::Project("todotxt-parser"), Tag::Context("workstation"), Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 4).unwrap()), - ]; - let tags = vec![&tags1]; + ]]; assert_eq!( ts, @@ -37,17 +36,18 @@ fn test_parse_from_str() { "2023-01-03 write tests for todotxt-parser +todotxt-parser @workstation due:2023-01-04\n\ x 2023-01-02 grocery shopping +groceries @supermarket due:2023-01-03"; let ts = parse_from_str(input).unwrap(); - let tags1 = vec![ - Tag::Project("todotxt-parser"), - Tag::Context("workstation"), - Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 4).unwrap()), + let tags = vec![ + vec![ + Tag::Project("todotxt-parser"), + Tag::Context("workstation"), + Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 4).unwrap()), + ], + vec![ + Tag::Project("groceries"), + Tag::Context("supermarket"), + Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 3).unwrap()), + ], ]; - let tags2 = vec![ - Tag::Project("groceries"), - Tag::Context("supermarket"), - Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 3).unwrap()), - ]; - let tags = vec![&tags1, &tags2]; assert_eq!( ts, blob - 69a61ac9e58924aa1b196262ccfc2ae3fdb240cd blob + 16a05dcca83a4a55bc6d9ee3e5d3ed449c0bd86d --- tests/types.rs +++ tests/types.rs @@ -27,5 +27,5 @@ fn test_task_try_from_str() { "measure space for +chapelShelving @chapel due:2016-05-30" ) ); - assert_eq!(res.tags(), &tags); + assert_eq!(res.tags(), tags); }