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}