Skip to main content

tatuin_core/
task_patch.rs

1// SPDX-License-Identifier: MIT
2
3use chrono::{Datelike, Local};
4use serde::{Deserialize, Serialize};
5
6use crate::task::{DateTimeUtc, Priority, State, Task as TaskTrait, datetime_to_str};
7use crate::time::{add_days, clear_time};
8
9#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
10pub enum DatePatchItem {
11    Today,
12    Tomorrow,
13    ThisWeekend,
14    NextWeek,
15    NoDate,
16    Custom(DateTimeUtc),
17}
18
19impl std::fmt::Display for DatePatchItem {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            DatePatchItem::Today => write!(f, "Today"),
23            DatePatchItem::Tomorrow => write!(f, "Tomorrow"),
24            DatePatchItem::ThisWeekend => write!(f, "This weekend"),
25            DatePatchItem::NextWeek => write!(f, "Next week (Monday)"),
26            DatePatchItem::NoDate => write!(f, "No date"),
27            DatePatchItem::Custom(d) => {
28                if d == &DateTimeUtc::default() {
29                    write!(f, "Custom")
30                } else {
31                    let tz = Local::now().timezone();
32                    write!(f, "Custom ({})", datetime_to_str(Some(*d), &tz))
33                }
34            }
35        }
36    }
37}
38
39impl DatePatchItem {
40    fn to_date(self, current_dt: &DateTimeUtc) -> Option<DateTimeUtc> {
41        let result = match self {
42            DatePatchItem::Today => Some(*current_dt),
43            DatePatchItem::Tomorrow => Some(add_days(current_dt, 1)),
44            DatePatchItem::ThisWeekend => match current_dt.weekday() {
45                chrono::Weekday::Sat | chrono::Weekday::Sun => Some(*current_dt),
46                wd => Some(add_days(current_dt, 5 - wd as u64)),
47            },
48            DatePatchItem::NextWeek => Some(add_days(current_dt, 7 - current_dt.weekday() as u64)),
49            DatePatchItem::NoDate => None,
50            DatePatchItem::Custom(dt) => Some(dt),
51        };
52
53        result.map(|d| clear_time(&d))
54    }
55
56    pub fn values() -> Vec<DatePatchItem> {
57        vec![
58            DatePatchItem::Today,
59            DatePatchItem::Tomorrow,
60            DatePatchItem::ThisWeekend,
61            DatePatchItem::NextWeek,
62            DatePatchItem::NoDate,
63        ]
64    }
65}
66
67impl From<DatePatchItem> for Option<DateTimeUtc> {
68    fn from(value: DatePatchItem) -> Option<DateTimeUtc> {
69        value.to_date(&chrono::Utc::now())
70    }
71}
72
73impl From<DateTimeUtc> for DatePatchItem {
74    fn from(dt: DateTimeUtc) -> Self {
75        let now = clear_time(&chrono::Utc::now());
76        match (dt - now).num_days() {
77            0 => DatePatchItem::Today,
78            1 => DatePatchItem::Tomorrow,
79            _ => DatePatchItem::Custom(dt),
80        }
81    }
82}
83
84#[derive(Debug, Clone, Default)]
85pub enum ValuePatch<T> {
86    #[default]
87    NotSet,
88    Empty,
89    Value(T),
90}
91
92impl<T> From<Option<T>> for ValuePatch<T> {
93    fn from(v: Option<T>) -> Self {
94        match v {
95            Some(v) => Self::Value(v),
96            None => Self::NotSet,
97        }
98    }
99}
100
101impl From<ValuePatch<DatePatchItem>> for ValuePatch<DateTimeUtc> {
102    fn from(value: ValuePatch<DatePatchItem>) -> Self {
103        match value {
104            ValuePatch::NotSet => ValuePatch::NotSet,
105            ValuePatch::Empty => ValuePatch::Empty,
106            ValuePatch::Value(due) => {
107                if let Some(d) = due.into() {
108                    ValuePatch::Value(d)
109                } else {
110                    ValuePatch::Empty
111                }
112            }
113        }
114    }
115}
116
117impl<T> ValuePatch<T>
118where
119    T: Clone,
120{
121    pub fn value(&self) -> Option<T> {
122        match self {
123            ValuePatch::NotSet | ValuePatch::Empty => None,
124            ValuePatch::Value(v) => Some(v.clone()),
125        }
126    }
127
128    pub fn ref_value(&self) -> Option<&T> {
129        match self {
130            ValuePatch::NotSet | ValuePatch::Empty => None,
131            ValuePatch::Value(v) => Some(v),
132        }
133    }
134
135    pub fn is_set(&self) -> bool {
136        !matches!(self, ValuePatch::NotSet)
137    }
138
139    pub fn map<U, F>(self, f: F) -> ValuePatch<U>
140    where
141        F: FnOnce(T) -> U,
142    {
143        match self {
144            ValuePatch::NotSet => ValuePatch::NotSet,
145            ValuePatch::Empty => ValuePatch::Empty,
146            ValuePatch::Value(x) => ValuePatch::Value(f(x)),
147        }
148    }
149}
150
151#[cfg(test)]
152mod test {
153    use super::ValuePatch;
154
155    #[test]
156    fn value_patch_is_set() {
157        type VP = ValuePatch<String>;
158        assert!(!VP::NotSet.is_set());
159        assert!(VP::Empty.is_set());
160        assert!(VP::Value("some".to_string()).is_set());
161    }
162}
163
164#[derive(Default)]
165pub struct TaskPatch {
166    pub task: Option<Box<dyn TaskTrait>>,
167    pub name: ValuePatch<String>,
168    pub description: ValuePatch<String>,
169    pub due: ValuePatch<DatePatchItem>,
170    pub scheduled: ValuePatch<DatePatchItem>,
171    pub priority: ValuePatch<Priority>,
172    pub state: ValuePatch<State>,
173}
174
175impl std::fmt::Display for TaskPatch {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        f.write_fmt(format_args!(
178            "TaskPatch {{ task_id: {}, task_title: {} state: {:?}, due: {:?}, scheduled: {:?}, priority: {:?}, name: {:?}, description: {:?}",
179            self.task.as_ref().map(|t| t.id()).unwrap_or("-".to_string()),
180            self.task.as_ref().map(|t| t.name().display()).unwrap_or("-".to_string()),
181            self.state,
182            self.due,
183            self.scheduled,
184            self.priority,
185            self.name,
186            self.description,
187        ))
188    }
189}
190
191impl std::fmt::Debug for TaskPatch {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        f.write_str(self.to_string().as_str())
194    }
195}
196
197impl TaskPatch {
198    pub fn is_empty(&self) -> bool {
199        !(self.name.is_set()
200            || self.description.is_set()
201            || self.due.is_set()
202            || self.scheduled.is_set()
203            || self.priority.is_set()
204            || self.state.is_set())
205    }
206
207    pub fn is_task(&self, task: &dyn TaskTrait) -> bool {
208        self.task
209            .as_ref()
210            .is_some_and(|t| t.id() == task.id() && t.provider() == task.provider())
211    }
212}
213
214impl Clone for TaskPatch {
215    fn clone(&self) -> Self {
216        Self {
217            task: if let Some(t) = self.task.as_ref() {
218                Some(t.clone_boxed())
219            } else {
220                None
221            },
222            name: self.name.clone(),
223            description: self.description.clone(),
224            due: self.due.clone(),
225            scheduled: self.scheduled.clone(),
226            priority: self.priority.clone(),
227            state: self.state.clone(),
228        }
229    }
230}
231
232pub struct PatchError {
233    pub task: Box<dyn TaskTrait>,
234    pub error: String,
235}
236
237impl PatchError {
238    pub fn is_task(&self, task: &dyn TaskTrait) -> bool {
239        self.task.id() == task.id() && self.task.provider() == task.provider()
240    }
241}
242
243impl std::fmt::Display for PatchError {
244    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
245        write!(f, "Error patching task with id {}: {}", self.task.id(), self.error)
246    }
247}
248
249impl Clone for PatchError {
250    fn clone(&self) -> Self {
251        Self {
252            task: self.task.clone_boxed(),
253            error: self.error.clone(),
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    fn dt_from_unixtime(secs: i64) -> DateTimeUtc {
263        clear_time(&DateTimeUtc::from_timestamp(secs, 0).unwrap())
264    }
265
266    #[test]
267    #[cfg_attr(miri, ignore)]
268    fn test_patch_due_to_date() {
269        struct Case<'a> {
270            name: &'a str,
271            due: DatePatchItem,
272            now: DateTimeUtc,
273            result: Option<DateTimeUtc>,
274        }
275        let cases: &[Case] = &[
276            Case {
277                name: "no date",
278                due: DatePatchItem::NoDate,
279                now: clear_time(&chrono::Utc::now()),
280                result: None,
281            },
282            Case {
283                name: "today",
284                due: DatePatchItem::Today,
285                now: clear_time(&chrono::Utc::now()),
286                result: Some(clear_time(&chrono::Utc::now())),
287            },
288            Case {
289                name: "tomorrow",
290                due: DatePatchItem::Tomorrow,
291                now: dt_from_unixtime(1749254400),
292                result: Some(dt_from_unixtime(1749340800)),
293            },
294            Case {
295                name: "this weekend for Monday",
296                due: DatePatchItem::ThisWeekend,
297                now: dt_from_unixtime(1748822400),
298                result: Some(dt_from_unixtime(1749254400)),
299            },
300            Case {
301                name: "this weekend for Friday",
302                due: DatePatchItem::ThisWeekend,
303                now: dt_from_unixtime(1749168000),
304                result: Some(dt_from_unixtime(1749254400)),
305            },
306            Case {
307                name: "this weekend for Saturday",
308                due: DatePatchItem::ThisWeekend,
309                now: dt_from_unixtime(1749254400),
310                result: Some(dt_from_unixtime(1749254400)),
311            },
312            Case {
313                name: "this weekend for Sunday",
314                due: DatePatchItem::ThisWeekend,
315                now: dt_from_unixtime(1749340800),
316                result: Some(dt_from_unixtime(1749340800)),
317            },
318            Case {
319                name: "next week for Monday",
320                due: DatePatchItem::NextWeek,
321                now: dt_from_unixtime(1748822400),
322                result: Some(dt_from_unixtime(1749427200)),
323            },
324            Case {
325                name: "next week for Friday",
326                due: DatePatchItem::NextWeek,
327                now: dt_from_unixtime(1749168000),
328                result: Some(dt_from_unixtime(1749427200)),
329            },
330            Case {
331                name: "next week for Saturday",
332                due: DatePatchItem::NextWeek,
333                now: dt_from_unixtime(1749254400),
334                result: Some(dt_from_unixtime(1749427200)),
335            },
336            Case {
337                name: "next week for Sunday",
338                due: DatePatchItem::NextWeek,
339                now: dt_from_unixtime(1749340800),
340                result: Some(dt_from_unixtime(1749427200)),
341            },
342        ];
343
344        for c in cases {
345            let result = c.due.to_date(&c.now);
346            assert_eq!(result, c.result, "Test '{}' was failed", c.name);
347        }
348    }
349}