commit - 82d62f1006b9df033509bb03d9cf3b550fe2d7bc
commit + 4846caa86f50134b6790df48298060c52944a501
blob - 7ef625469dadf66107a9d7721f134eac58a93593
blob + a6008ec414cdc9cce23379b272b25193e1ae49ed
--- Cargo.toml
+++ Cargo.toml
[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
#[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<NaiveDate>,
+ creation_date: Option<NaiveDate>,
/// Description of the [`Task`].
description: Cow<'a, str>,
}
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"),
}
}
+ /// 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<NaiveDate> {
self.creation_date
}
+ /// Set the creation date, returning a new [`Task`].
#[must_use]
+ pub fn set_creation_date(&self, creation_date: Option<NaiveDate>) -> 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<Tag> {
parser::description_tags(self.description.as_ref())
.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> {
.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<Recurring> {
}
}
+#[macro_export]
+macro_rules! regex {
+ ($re:literal $(,)?) => {{
+ static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new();
+ RE.get_or_init(|| regex::Regex::new($re).unwrap())
+ }};
+}
+
#[cfg(test)]
-mod test {
+mod tests {
use super::*;
#[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())]
+ );
+ }
}