1use 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}