saturn_cli/parsers/
entry.rs

1use super::time::{parse_date, parse_time};
2use crate::record::{Record, RecurringRecord};
3use anyhow::{anyhow, Result};
4use chrono::Duration;
5use fancy_duration::FancyDuration;
6
7#[derive(Debug, Clone)]
8pub struct EntryParser {
9    args: Vec<String>,
10    use_24h_time: bool,
11}
12
13impl EntryParser {
14    pub fn new(args: Vec<String>, use_24h_time: bool) -> Self {
15        Self { args, use_24h_time }
16    }
17
18    pub fn to_record(&self) -> Result<EntryRecord> {
19        parse_entry(self.args.clone(), self.use_24h_time)
20    }
21}
22
23pub enum EntryState {
24    Recur,
25    Date,
26    Time,
27    TimeAt,
28    TimeScheduled,
29    TimeScheduledHalf,
30    Notify,
31    NotifyTime,
32    Detail,
33}
34
35#[derive(Debug, PartialEq)]
36pub struct EntryRecord {
37    record: Record,
38    recurrence: Option<RecurringRecord>,
39}
40
41impl EntryRecord {
42    pub fn record(&self) -> Record {
43        self.record.clone()
44    }
45
46    pub fn recurrence(&self) -> Option<RecurringRecord> {
47        self.recurrence.clone()
48    }
49}
50
51fn parse_entry(args: Vec<String>, use_24h_time: bool) -> Result<EntryRecord> {
52    let mut record = Record::build();
53    let mut state = EntryState::Date;
54
55    let mut scheduled_first: Option<chrono::NaiveTime> = None;
56    let mut recurrence: Option<FancyDuration<Duration>> = None;
57
58    for arg in &args {
59        match state {
60            EntryState::Recur => {
61                recurrence = Some(FancyDuration::<Duration>::parse(arg)?);
62                state = EntryState::Date;
63            }
64            EntryState::Date => {
65                match arg.to_lowercase().as_str() {
66                    "recur" => {
67                        state = EntryState::Recur;
68                    }
69                    _ => {
70                        record.set_date(parse_date(arg.to_string())?);
71                        state = EntryState::Time;
72                    }
73                };
74            }
75            EntryState::Time => match arg.as_str() {
76                "all" => {
77                    record.set_all_day();
78                    state = EntryState::TimeAt
79                }
80                "at" => state = EntryState::TimeAt,
81                "from" => state = EntryState::TimeScheduled,
82                _ => return Err(anyhow!("Time must be 'from' or 'at'")),
83            },
84            EntryState::TimeAt => {
85                if arg != "day" {
86                    record.set_at(Some(parse_time(arg.to_string(), !use_24h_time)?));
87                }
88                state = EntryState::Notify;
89            }
90            EntryState::TimeScheduled => {
91                scheduled_first = Some(parse_time(arg.to_string(), !use_24h_time)?);
92                state = EntryState::TimeScheduledHalf;
93            }
94            EntryState::TimeScheduledHalf => match arg.as_str() {
95                "to" | "until" => {}
96                _ => {
97                    record.set_scheduled(Some((
98                        scheduled_first.unwrap(),
99                        parse_time(arg.to_string(), !use_24h_time)?,
100                    )));
101                    state = EntryState::Notify;
102                }
103            },
104            EntryState::Notify => match arg.as_str() {
105                "notify" => state = EntryState::NotifyTime,
106                _ => {
107                    record.set_detail(arg.to_string());
108                    state = EntryState::Detail;
109                }
110            },
111            EntryState::NotifyTime => match arg.as_str() {
112                "me" => {}
113                _ => {
114                    let duration = FancyDuration::<Duration>::parse(arg)?;
115                    record.add_notification(duration.duration());
116                    state = EntryState::Detail;
117                }
118            },
119            EntryState::Detail => {
120                if record.detail().is_empty() {
121                    record.set_detail(arg.to_string());
122                } else {
123                    record.set_detail(format!("{} {}", record.detail(), arg));
124                }
125            }
126        }
127    }
128
129    Ok(EntryRecord {
130        record: record.clone(),
131        recurrence: recurrence.map_or_else(|| None, |x| Some(RecurringRecord::new(record, x))),
132    })
133}
134
135#[cfg(test)]
136mod tests {
137    #[test]
138    fn test_parse_entry() {
139        use super::parse_entry;
140        use crate::{record::Record, time::now};
141        use chrono::{Datelike, TimeDelta, Timelike};
142
143        let pm = now().hour() >= 12;
144        let record = Record::build();
145
146        let mut today = record.clone();
147        today
148            .set_date(chrono::Local::now().naive_local().date())
149            .set_at(Some(
150                chrono::NaiveTime::from_hms_opt(if pm { 20 } else { 8 }, 0, 0).unwrap(),
151            ))
152            .add_notification(chrono::TimeDelta::try_minutes(5).unwrap_or_default())
153            .set_detail("Test Today".to_string());
154
155        let mut soda = record.clone();
156        soda.set_date(chrono::NaiveDate::from_ymd_opt(now().year(), 8, 5).unwrap())
157            .set_at(Some(
158                chrono::NaiveTime::from_hms_opt(if pm { 20 } else { 8 }, 0, 0).unwrap(),
159            ))
160            .add_notification(chrono::TimeDelta::try_minutes(5).unwrap_or_default())
161            .set_detail("Get a Soda".to_string());
162
163        let mut relax = record.clone();
164        relax
165            .set_date((now() + TimeDelta::try_days(1).unwrap_or_default()).date_naive())
166            .set_at(Some(chrono::NaiveTime::from_hms_opt(16, 0, 0).unwrap()))
167            .set_detail("Relax".to_string());
168
169        let mut birthday = record.clone();
170        birthday
171            .set_date(chrono::NaiveDate::from_ymd_opt(now().year(), 10, 23).unwrap())
172            .set_at(Some(chrono::NaiveTime::from_hms_opt(7, 30, 0).unwrap()))
173            .add_notification(chrono::TimeDelta::try_hours(1).unwrap_or_default())
174            .set_detail("Tell my daughter 'happy birthday'".to_string());
175
176        let mut new_year = record.clone();
177        new_year
178            .set_date(chrono::NaiveDate::from_ymd_opt(now().year(), 1, 1).unwrap())
179            .set_at(Some(chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap()))
180            .set_detail("Happy new year!".to_string());
181
182        let mut christmas = record.clone();
183        christmas
184            .set_date(chrono::NaiveDate::from_ymd_opt(now().year(), 12, 25).unwrap())
185            .set_scheduled(Some((
186                chrono::NaiveTime::from_hms_opt(7, 0, 0).unwrap(),
187                chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
188            )))
189            .set_detail("Christmas Morning".to_string());
190
191        let table = vec![
192            ("today at 8 notify me 5m Test Today", today),
193            ("08/05 at 8 notify me 5m Get a Soda", soda),
194            ("tomorrow at 4pm Relax", relax),
195            (
196                "10/23 at 7:30am notify 1h Tell my daughter 'happy birthday'",
197                birthday,
198            ),
199            ("1/1 at 12am Happy new year!", new_year),
200            ("12/25 from 7am to 12pm Christmas Morning", christmas),
201        ];
202
203        for (to_parse, t) in table {
204            assert_eq!(
205                parse_entry(
206                    to_parse
207                        .split(" ")
208                        .map(|s| s.to_string())
209                        .collect::<Vec<String>>(),
210                    false,
211                )
212                .unwrap()
213                .record,
214                t,
215            )
216        }
217    }
218}