saturn_cli/
record.rs

1use crate::db::DB;
2use anyhow::{anyhow, Result};
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6pub type Schedule = (chrono::NaiveTime, chrono::NaiveTime);
7pub type Notifications = Vec<fancy_duration::FancyDuration<chrono::Duration>>;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub enum RecordType {
11    At,
12    Schedule,
13    AllDay,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17pub struct PresentedSchedule {
18    start: chrono::NaiveTime,
19    stop: chrono::NaiveTime,
20}
21
22impl std::fmt::Display for PresentedSchedule {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        f.write_str(&format!(
25            "{} - {}",
26            self.start.format("%H:%M"),
27            self.stop.format("%H:%M")
28        ))
29    }
30}
31
32impl From<PresentedSchedule> for Schedule {
33    fn from(ps: PresentedSchedule) -> Self {
34        (ps.start, ps.stop)
35    }
36}
37
38impl From<Schedule> for PresentedSchedule {
39    fn from(s: Schedule) -> Self {
40        Self {
41            start: s.0,
42            stop: s.1,
43        }
44    }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
48pub struct Fields(BTreeMap<String, Vec<String>>);
49
50impl std::ops::Deref for Fields {
51    type Target = BTreeMap<String, Vec<String>>;
52
53    fn deref(&self) -> &Self::Target {
54        &self.0
55    }
56}
57
58impl std::fmt::Display for Fields {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        if self.is_empty() {
61            return Ok(());
62        }
63
64        let mut fields = String::new();
65        for (key, value) in &self.0 {
66            if value.len() > 1 {
67                fields += &format!("[{}: {}], ", key, value.len());
68            } else if !value.is_empty() {
69                fields += &format!("[{}: {}], ", key, value[0]);
70            }
71        }
72
73        // strip trailing whitespace + comma
74        fields.remove(fields.len() - 1);
75        fields.remove(fields.len() - 1);
76        f.write_str(&fields)
77    }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct PresentedRecord {
82    pub date: chrono::NaiveDate,
83    #[serde(rename = "type")]
84    pub typ: RecordType,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub at: Option<chrono::NaiveTime>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub scheduled: Option<PresentedSchedule>,
89    pub detail: String,
90    pub fields: Fields,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub notifications: Option<Notifications>,
93    pub completed: bool,
94}
95
96impl From<Record> for PresentedRecord {
97    fn from(value: Record) -> Self {
98        Self {
99            date: value.date,
100            typ: value.typ,
101            at: value.at,
102            scheduled: value.scheduled.map(|x| x.into()),
103            detail: value.detail,
104            fields: value.fields,
105            notifications: value.notifications,
106            completed: value.completed,
107        }
108    }
109}
110
111impl PresentedRecord {
112    pub fn to_record(
113        self,
114        primary_key: u64,
115        recurrence_key: Option<u64>,
116        internal_key: Option<String>,
117        internal_recurrence_key: Option<String>,
118    ) -> Record {
119        Record {
120            primary_key,
121            recurrence_key,
122            internal_key,
123            internal_recurrence_key,
124            date: self.date,
125            typ: self.typ,
126            at: self.at,
127            scheduled: self.scheduled.map(|x| x.into()),
128            detail: self.detail,
129            fields: self.fields,
130            notifications: self.notifications,
131            completed: self.completed,
132        }
133    }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137pub struct PresentedRecurringRecord {
138    pub record: PresentedRecord,
139    pub recurrence: fancy_duration::FancyDuration<chrono::Duration>,
140}
141
142impl From<RecurringRecord> for PresentedRecurringRecord {
143    fn from(value: RecurringRecord) -> Self {
144        Self {
145            record: value.record.into(),
146            recurrence: value.recurrence,
147        }
148    }
149}
150impl PresentedRecurringRecord {
151    pub fn to_record(
152        self,
153        primary_key: u64,
154        recurrence_key: u64,
155        internal_key: Option<String>,
156        internal_recurrence_key: Option<String>,
157    ) -> RecurringRecord {
158        RecurringRecord {
159            internal_key: internal_key.clone(),
160            recurrence_key,
161            record: self.record.to_record(
162                primary_key,
163                Some(recurrence_key),
164                internal_key,
165                internal_recurrence_key,
166            ),
167            recurrence: self.recurrence,
168        }
169    }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
173pub struct RecurringRecord {
174    record: Record,
175    recurrence: fancy_duration::FancyDuration<chrono::Duration>,
176    recurrence_key: u64,
177    internal_key: Option<String>,
178}
179
180#[derive(Clone, Debug)]
181enum RuleFrequency {
182    Daily,
183    Monthly,
184    Weekly,
185    Yearly,
186}
187
188impl ToString for RuleFrequency {
189    fn to_string(&self) -> String {
190        match self {
191            RuleFrequency::Daily => "daily",
192            RuleFrequency::Monthly => "monthly",
193            RuleFrequency::Yearly => "yearly",
194            RuleFrequency::Weekly => "weekly",
195        }
196        .to_uppercase()
197    }
198}
199
200impl std::str::FromStr for RuleFrequency {
201    type Err = anyhow::Error;
202
203    fn from_str(s: &str) -> Result<Self, Self::Err> {
204        match s.to_lowercase().as_str() {
205            "daily" => Ok(RuleFrequency::Daily),
206            "yearly" => Ok(RuleFrequency::Yearly),
207            "monthly" => Ok(RuleFrequency::Monthly),
208            "weekly" => Ok(RuleFrequency::Weekly),
209            _ => Err(anyhow!("Invalid frequency {}", s)),
210        }
211    }
212}
213
214impl RecurringRecord {
215    pub fn new(
216        record: Record,
217        recurrence: fancy_duration::FancyDuration<chrono::Duration>,
218    ) -> Self {
219        Self {
220            record,
221            recurrence,
222            recurrence_key: 0,
223            internal_key: None,
224        }
225    }
226
227    pub fn from_rrule(record: Record, rrule: String) -> Result<Self> {
228        let parts = rrule.split(':').collect::<Vec<&str>>();
229
230        if parts[0] == "RRULE" {
231            let tokens = parts[1]
232                .split(';')
233                .map(|s| s.split('=').collect::<Vec<&str>>());
234            let mut freq: Option<RuleFrequency> = None;
235            let mut interval: Option<i64> = None;
236
237            for pair in tokens {
238                match pair[0] {
239                    "FREQ" => {
240                        freq = Some(pair[1].parse()?);
241                    }
242                    "INTERVAL" => {
243                        interval = Some(pair[1].parse()?);
244                    }
245                    _ => {}
246                }
247
248                if freq.is_some() && interval.is_some() {
249                    break;
250                }
251            }
252
253            if let Some(freq) = freq {
254                if let Some(interval) = interval {
255                    return Ok(Self::new(
256                        record,
257                        fancy_duration::FancyDuration::new(match freq {
258                            RuleFrequency::Daily => chrono::TimeDelta::try_days(interval).unwrap_or_default(),
259                            RuleFrequency::Yearly => chrono::TimeDelta::try_weeks(interval).unwrap_or_default() * 52,
260                            RuleFrequency::Weekly => chrono::TimeDelta::try_weeks(interval).unwrap_or_default(),
261                            RuleFrequency::Monthly => chrono::TimeDelta::try_days(interval).unwrap_or_default() * 30,
262                        }),
263                    ));
264                }
265            }
266        }
267
268        Err(anyhow!("Recurring data cannot be parsed"))
269    }
270
271    pub fn to_rrule(&self) -> String {
272        let recur = self.recurrence.duration();
273
274        let freq = if recur < chrono::TimeDelta::try_days(30).unwrap_or_default() {
275            ("DAILY", recur.num_days())
276        } else if recur < chrono::TimeDelta::try_weeks(52).unwrap_or_default() {
277            ("MONTHLY", recur.num_days() / 30)
278        } else {
279            ("YEARLY", recur.num_weeks() * 52)
280        };
281
282        format!("RRULE:FREQ={};INTERVAL={}", freq.0, freq.1)
283    }
284
285    pub fn record(&mut self) -> &mut Record {
286        &mut self.record
287    }
288
289    pub fn recurrence(&self) -> fancy_duration::FancyDuration<chrono::Duration> {
290        self.recurrence.clone()
291    }
292
293    pub fn recurrence_key(&self) -> u64 {
294        self.recurrence_key
295    }
296
297    pub fn set_record(&mut self, record: Record) {
298        self.record = record;
299    }
300
301    pub fn set_recurrence_key(&mut self, key: u64) {
302        self.recurrence_key = key;
303        self.record().set_recurrence_key(Some(key));
304    }
305
306    pub fn internal_key(&self) -> Option<String> {
307        self.internal_key.clone()
308    }
309
310    pub fn set_internal_key(&mut self, key: Option<String>) {
311        self.internal_key = key.clone();
312        self.record().set_internal_recurrence_key(key);
313    }
314
315    pub fn record_from(&self, primary_key: u64, from: chrono::NaiveDateTime) -> Record {
316        let mut record = self.record.clone();
317        record.set_primary_key(primary_key);
318        record.set_recurrence_key(Some(self.recurrence_key));
319        record.set_internal_recurrence_key(self.internal_key.clone());
320        record.set_date(from.date());
321        match record.record_type() {
322            RecordType::At => {
323                record.set_at(Some(from.time()));
324            }
325            RecordType::AllDay => {}
326            RecordType::Schedule => {
327                let schedule = record.scheduled().unwrap();
328                let duration = schedule.1 - schedule.0;
329                record.set_scheduled(Some((from.time(), from.time() + duration)));
330            }
331        };
332        record
333    }
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
337pub struct Record {
338    primary_key: u64,
339    recurrence_key: Option<u64>,
340    internal_key: Option<String>,
341    internal_recurrence_key: Option<String>,
342    date: chrono::NaiveDate,
343    typ: RecordType,
344    at: Option<chrono::NaiveTime>,
345    scheduled: Option<Schedule>,
346    detail: String,
347    fields: Fields,
348    notifications: Option<Notifications>,
349    completed: bool,
350}
351
352impl Default for Record {
353    fn default() -> Self {
354        let now = chrono::Local::now();
355        Self {
356            primary_key: 0,
357            recurrence_key: None,
358            internal_key: None,
359            internal_recurrence_key: None,
360            date: now.date_naive(),
361            typ: RecordType::AllDay,
362            at: None,
363            scheduled: None,
364            detail: String::new(),
365            fields: Fields::default(),
366            notifications: None,
367            completed: false,
368        }
369    }
370}
371
372impl Record {
373    pub fn primary_key(&self) -> u64 {
374        self.primary_key
375    }
376
377    pub fn recurrence_key(&self) -> Option<u64> {
378        self.recurrence_key
379    }
380
381    pub fn internal_recurrence_key(&self) -> Option<String> {
382        self.internal_recurrence_key.clone()
383    }
384
385    pub fn internal_key(&self) -> Option<String> {
386        self.internal_key.clone()
387    }
388
389    pub fn set_internal_key(&mut self, key: Option<String>) {
390        self.internal_key = key
391    }
392
393    pub fn record_type(&self) -> RecordType {
394        self.typ.clone()
395    }
396
397    pub fn datetime(&self) -> chrono::DateTime<chrono::Local> {
398        let time = match self.record_type() {
399            RecordType::At => self.at.unwrap(),
400            RecordType::AllDay => chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
401            RecordType::Schedule => self.scheduled.unwrap().0,
402        };
403
404        chrono::NaiveDateTime::new(self.date, time)
405            .and_local_timezone(chrono::Local::now().timezone())
406            .unwrap()
407    }
408
409    pub fn completed(&self) -> bool {
410        self.completed
411    }
412
413    pub fn date(&self) -> chrono::NaiveDate {
414        self.date
415    }
416
417    pub fn at(&self) -> Option<chrono::NaiveTime> {
418        self.at
419    }
420
421    pub fn scheduled(&self) -> Option<Schedule> {
422        self.scheduled
423    }
424
425    pub fn all_day(&self) -> bool {
426        matches!(self.typ, RecordType::AllDay)
427    }
428
429    pub fn detail(&self) -> String {
430        self.detail.clone()
431    }
432
433    pub fn fields(&self) -> Fields {
434        self.fields.clone()
435    }
436
437    pub fn set_fields(&mut self, fields: Fields) {
438        self.fields = fields
439    }
440
441    pub fn notifications(&self) -> Option<Notifications> {
442        self.notifications.clone()
443    }
444
445    pub fn build() -> Self {
446        Self::default()
447    }
448
449    pub async fn record(&self, mut db: crate::db::memory::MemoryDB) -> Result<()> {
450        db.record(self.clone()).await
451    }
452
453    pub fn set_internal_recurrence_key(&mut self, internal_recurrence_key: Option<String>) {
454        self.internal_recurrence_key = internal_recurrence_key
455    }
456
457    pub fn set_primary_key(&mut self, primary_key: u64) -> &mut Self {
458        self.primary_key = primary_key;
459        self
460    }
461
462    pub fn set_recurrence_key(&mut self, key: Option<u64>) -> &mut Self {
463        self.recurrence_key = key;
464        self
465    }
466
467    pub fn set_record_type(&mut self, typ: RecordType) -> &mut Self {
468        self.typ = typ;
469        self
470    }
471
472    pub fn set_all_day(&mut self) -> &mut Self {
473        self.at = None;
474        self.scheduled = None;
475        self.typ = RecordType::AllDay;
476        self
477    }
478
479    pub fn set_completed(&mut self, completed: bool) -> &mut Self {
480        self.completed = completed;
481        self
482    }
483
484    pub fn set_date(&mut self, date: chrono::NaiveDate) -> &mut Self {
485        self.date = date;
486        self
487    }
488
489    pub fn set_at(&mut self, at: Option<chrono::NaiveTime>) -> &mut Self {
490        self.at = at;
491        self.scheduled = None;
492        self.typ = RecordType::At;
493        self
494    }
495
496    pub fn set_scheduled(&mut self, schedule: Option<Schedule>) -> &mut Self {
497        self.scheduled = schedule;
498        self.at = None;
499        self.typ = RecordType::Schedule;
500        self
501    }
502
503    pub fn set_detail(&mut self, detail: String) -> &mut Self {
504        self.detail = detail;
505        self
506    }
507
508    pub fn add_field(&mut self, field: String, content: String) -> &mut Self {
509        let mut v = self.fields.0.get(&field).unwrap_or(&Vec::new()).to_owned();
510        v.push(content);
511        self.fields.0.insert(field, v);
512        self
513    }
514
515    pub fn get_field(&self, field: String) -> Option<Vec<String>> {
516        self.fields.0.get(&field).cloned()
517    }
518
519    pub fn add_notification(&mut self, notification: chrono::Duration) -> &mut Self {
520        let notification = fancy_duration::FancyDuration::new(notification);
521        if let Some(notifications) = &mut self.notifications {
522            notifications.push(notification)
523        } else {
524            self.notifications = Some(vec![notification])
525        }
526
527        self
528    }
529
530    pub fn set_notifications(&mut self, notifications: Option<Notifications>) {
531        self.notifications = notifications
532    }
533}
534
535pub fn sort_records(a: &Record, b: &Record) -> std::cmp::Ordering {
536    let cmp = a.date().cmp(&b.date());
537    if cmp == std::cmp::Ordering::Equal {
538        match a.record_type() {
539            RecordType::At => {
540                if let Some(a_at) = a.at() {
541                    if let Some(b_at) = b.at() {
542                        a_at.cmp(&b_at)
543                    } else if let Some(b_schedule) = b.scheduled() {
544                        a_at.cmp(&b_schedule.0)
545                    } else {
546                        std::cmp::Ordering::Equal
547                    }
548                } else {
549                    std::cmp::Ordering::Equal
550                }
551            }
552            RecordType::AllDay => {
553                if b.record_type() == RecordType::AllDay {
554                    a.primary_key().cmp(&b.primary_key())
555                } else {
556                    std::cmp::Ordering::Less
557                }
558            }
559            RecordType::Schedule => {
560                if let Some(a_schedule) = a.scheduled() {
561                    if let Some(b_schedule) = b.scheduled() {
562                        a_schedule.0.cmp(&b_schedule.0)
563                    } else if let Some(b_at) = b.at() {
564                        a_schedule.0.cmp(&b_at)
565                    } else {
566                        std::cmp::Ordering::Equal
567                    }
568                } else {
569                    std::cmp::Ordering::Equal
570                }
571            }
572        }
573    } else {
574        cmp
575    }
576}