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 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 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 } 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 (&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, (_, _, None) => false, }
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, }
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(); 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 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
180pub 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(); 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 let due = parse_date(&due_date_regex, &name_with_potential_dates);
197 let scheduled = parse_date(&scheduled_date_regex, &name_with_potential_dates);
199 let start = parse_date(&start_date_regex, &name_with_potential_dates);
201
202 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 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}