marktask/
lib.rs

1use chrono::{Local, NaiveDate};
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4pub mod dates;
5mod serializers {
6    use chrono::NaiveDate;
7    use serde::{Deserializer, Serializer};
8    use std::fmt;
9
10    // Implement the serialization function for NaiveDate
11    pub fn serialize<S>(date: &Option<NaiveDate>, serializer: S) -> Result<S::Ok, S::Error>
12    where
13        S: Serializer,
14    {
15        match date {
16            Some(d) => serializer.serialize_some(&d.format("%Y-%m-%d").to_string()),
17            None => serializer.serialize_none(),
18        }
19    }
20
21    // Implement the deserialization function for NaiveDate
22    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<NaiveDate>, D::Error>
23    where
24        D: Deserializer<'de>,
25    {
26        use chrono::format::ParseError;
27        use serde::de::{self, Visitor};
28
29        struct DateVisitor;
30
31        impl<'de> Visitor<'de> for DateVisitor {
32            type Value = Option<NaiveDate>;
33
34            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
35                formatter.write_str("a formatted date string")
36            }
37
38            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
39            where
40                E: de::Error,
41            {
42                NaiveDate::parse_from_str(value, "%Y-%m-%d")
43                    .map(Some)
44                    .map_err(de::Error::custom)
45            }
46        }
47
48        deserializer.deserialize_option(DateVisitor)
49    }
50}
51
52pub trait Filter {
53    fn apply<'a>(&self, tasks: Vec<&'a Task>) -> Vec<&'a Task>;
54}
55
56pub struct OverdueFilter {
57    pub show_overdue: bool,
58}
59
60impl Filter for OverdueFilter {
61    fn apply<'a>(&self, tasks: Vec<&'a Task>) -> Vec<&'a Task> {
62        if self.show_overdue {
63            tasks // Directly return the tasks if filtering is not needed
64        } else {
65            tasks.into_iter().filter(|&task| !task.overdue).collect()
66        }
67    }
68}
69
70pub struct DateRangeFilter {
71    pub from_date: Option<NaiveDate>,
72    pub to_date: Option<NaiveDate>,
73}
74
75impl Filter for DateRangeFilter {
76    fn apply<'a>(&self, tasks: Vec<&'a Task>) -> Vec<&'a Task> {
77        tasks.into_iter().filter(|&task| {
78            // Match against the combination of 'from' date, 'to' date, and the task's due date
79            match (&self.from_date, &self.to_date, &task.due) {
80                (Some(from), Some(to), Some(date)) => date >= from && date <= to,
81                (Some(from), None, Some(date)) => date >= from,
82                (None, Some(to), Some(date)) => date <= to,
83                (None, None, _) => true, // Include tasks when no date filter is applied
84                (_, _, None) => false, // Exclude tasks without due dates when any date filter is applied
85            }
86        }).collect()
87    }
88}
89
90
91
92pub struct FilterPipeline {
93    pub filters: Vec<Box<dyn Filter>>,
94}
95
96impl FilterPipeline {
97    pub fn new() -> Self {
98        FilterPipeline {
99            filters: Vec::new(),
100        }
101    }
102
103    pub fn add_filter(&mut self, filter: Box<dyn Filter>) {
104        self.filters.push(filter);
105    }
106
107    pub fn apply<'a>(&self, tasks: Vec<&'a Task>) -> Vec<&'a Task> {
108        self.filters
109            .iter()
110            .fold(tasks, |acc, filter| filter.apply(acc))
111    }
112}
113
114#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
115pub enum Priority {
116    Highest,
117    High,
118    Medium,
119    Low,
120    Lowest,
121    None, // Represents no specific priority
122}
123
124#[derive(Serialize)]
125pub struct Task {
126    pub name: String,
127    pub completed: bool,
128    #[serde(
129        serialize_with = "serializers::serialize",
130        deserialize_with = "serializers::deserialize",
131        skip_serializing_if = "Option::is_none"
132    )]
133    pub due: Option<NaiveDate>,
134    #[serde(
135        serialize_with = "serializers::serialize",
136        deserialize_with = "serializers::deserialize",
137        skip_serializing_if = "Option::is_none"
138    )]
139    pub scheduled: Option<NaiveDate>,
140    #[serde(
141        serialize_with = "serializers::serialize",
142        deserialize_with = "serializers::deserialize",
143        skip_serializing_if = "Option::is_none"
144    )]
145    pub start: Option<NaiveDate>,
146    pub overdue: bool,
147    pub priority: Priority,
148}
149
150fn clean_description(description: &str) -> String {
151    let re = Regex::new(r"\s+").unwrap(); // Matches one or more whitespace characters
152    re.replace_all(description.trim(), " ").to_string()
153}
154
155pub fn parse_priority(description: &str) -> (String, Priority) {
156    let (priority, signifier) = if description.contains("🔺") {
157        (Priority::Highest, "🔺")
158    } else if description.contains("⏫") {
159        (Priority::High, "⏫")
160    } else if description.contains("🔼") {
161        (Priority::Medium, "🔼")
162    } else if description.contains("🔽") {
163        (Priority::Low, "🔽")
164    } else if description.contains("⏬") {
165        (Priority::Lowest, "⏬")
166    } else {
167        (Priority::None, "")
168    };
169
170    // Remove the signifier from the description to clean it up
171    let clean_description = if !signifier.is_empty() {
172        description.replace(signifier, "").trim().to_string()
173    } else {
174        description.to_string()
175    };
176
177    (clean_description, priority)
178}
179
180/// Parses the input text into a vector of `Task` objects.
181pub fn parse_input(input: &str) -> Vec<Task> {
182    let task_regex = Regex::new(r"^\s*-\s*\[(\s|x)]\s*(.*)").unwrap();
183    let due_date_regex = Regex::new(r"📅 (\d{4}-\d{2}-\d{2})").unwrap();
184    let scheduled_date_regex = Regex::new(r"⏳ (\d{4}-\d{2}-\d{2})").unwrap();
185    let start_date_regex = Regex::new(r"🛫 (\d{4}-\d{2}-\d{2})").unwrap(); // Regex for start dates
186
187    input
188        .lines()
189        .filter_map(|line| {
190            task_regex.captures(line).map(|caps| {
191                let completed = caps.get(1).map_or(false, |m| m.as_str() == "x");
192                let mut name_with_potential_dates =
193                    caps.get(2).map_or("", |m| m.as_str()).to_string();
194
195                // Extract and parse the due date
196                let due = parse_date(&due_date_regex, &name_with_potential_dates);
197                // Extract and parse the scheduled date
198                let scheduled = parse_date(&scheduled_date_regex, &name_with_potential_dates);
199                // Extract and parse the start date
200                let start = parse_date(&start_date_regex, &name_with_potential_dates);
201
202                // Clean the task name by removing date strings
203                name_with_potential_dates = remove_date_strings(
204                    &[&due_date_regex, &scheduled_date_regex, &start_date_regex],
205                    name_with_potential_dates,
206                );
207
208                let overdue = due.map_or(false, |due_date| due_date < Local::today().naive_local());
209
210                let (description_without_priorities, priority) =
211                    parse_priority(&name_with_potential_dates);
212
213                // Clean up the remaining description
214                let cleaned_description = clean_description(&description_without_priorities);
215
216                Task {
217                    name: cleaned_description,
218                    completed,
219                    due,
220                    scheduled,
221                    start,
222                    overdue,
223                    priority: priority,
224                }
225            })
226        })
227        .collect()
228}
229
230fn parse_date(date_regex: &Regex, text: &str) -> Option<NaiveDate> {
231    date_regex.captures(text).and_then(|caps| {
232        caps.get(1)
233            .and_then(|m| NaiveDate::parse_from_str(m.as_str(), "%Y-%m-%d").ok())
234    })
235}
236
237fn remove_date_strings(regexes: &[&Regex], mut text: String) -> String {
238    for regex in regexes {
239        text = regex.replace_all(&text, "").to_string();
240    }
241    text
242}