hledger_parser/component/period/
interval.rs

1use chumsky::prelude::*;
2
3use crate::{component::whitespace::whitespace, state::State};
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum Interval {
7    // Every N days
8    NthDay(u32),
9    // Every N Weeks
10    NthWeek(u32),
11    // Every N quarters
12    NthQuarter(u32),
13    // Every N months
14    NthMonth(u32),
15    // Every N years
16    NthYear(u32),
17    // Weekly on a week day
18    Weekday(chrono::Weekday),
19}
20
21// TODO:
22// every Nth day [of month] (31st day will be adjusted to each month's last day)
23// every Nth WEEKDAYNAME [of month]
24// Yearly on a custom month and day:
25//
26// every MM/DD [of year] (month number and day of month number)
27// every MONTHNAME DDth [of year] (full or three-letter english month name, case insensitive, and day of month number)
28// every DDth MONTHNAME [of year] (equivalent to the above)
29pub fn interval<'a>() -> impl Parser<'a, &'a str, Interval, extra::Full<Rich<'a, char>, State, ()>>
30{
31    let word = choice([
32        just("daily").to(Interval::NthDay(1)),
33        just("weekly").to(Interval::NthWeek(1)),
34        just("biweekly").to(Interval::NthWeek(2)),
35        just("fortnightly").to(Interval::NthWeek(2)),
36        just("monthly").to(Interval::NthMonth(1)),
37        just("bimonthly").to(Interval::NthMonth(2)),
38        just("quarterly").to(Interval::NthQuarter(1)),
39        just("yearly").to(Interval::NthQuarter(1)),
40    ]);
41
42    word.or(every()).or(day_of_week())
43}
44
45fn day_of_week<'a>() -> impl Parser<'a, &'a str, Interval, extra::Full<Rich<'a, char>, State, ()>> {
46    let monday = just("monday")
47        .ignored()
48        .or(just("mon").ignored())
49        .or(just("1st")
50            .then(whitespace().repeated().at_least(1))
51            .then(just("day"))
52            .then(whitespace().repeated().at_least(1))
53            .then(just("of"))
54            .then(whitespace().repeated().at_least(1))
55            .then(just("week"))
56            .ignored())
57        .map(|()| Interval::Weekday(chrono::Weekday::Mon));
58    let tuesday = just("tuesday")
59        .ignored()
60        .or(just("tue").ignored())
61        .or(just("2nd")
62            .then(whitespace().repeated().at_least(1))
63            .then(just("day"))
64            .then(whitespace().repeated().at_least(1))
65            .then(just("of"))
66            .then(whitespace().repeated().at_least(1))
67            .then(just("week"))
68            .ignored())
69        .map(|()| Interval::Weekday(chrono::Weekday::Tue));
70    let wednesday = just("wednesday")
71        .ignored()
72        .or(just("wed").ignored())
73        .or(just("3rd")
74            .then(whitespace().repeated().at_least(1))
75            .then(just("day"))
76            .then(whitespace().repeated().at_least(1))
77            .then(just("of"))
78            .then(whitespace().repeated().at_least(1))
79            .then(just("week"))
80            .ignored())
81        .map(|()| Interval::Weekday(chrono::Weekday::Wed));
82    let thursday = just("thursday")
83        .ignored()
84        .or(just("thu").ignored())
85        .or(just("3rd")
86            .then(whitespace().repeated().at_least(1))
87            .then(just("day"))
88            .then(whitespace().repeated().at_least(1))
89            .then(just("of"))
90            .then(whitespace().repeated().at_least(1))
91            .then(just("week"))
92            .ignored())
93        .map(|()| Interval::Weekday(chrono::Weekday::Thu));
94    let friday = just("friday")
95        .ignored()
96        .or(just("fri").ignored())
97        .or(just("3rd")
98            .then(whitespace().repeated().at_least(1))
99            .then(just("day"))
100            .then(whitespace().repeated().at_least(1))
101            .then(just("of"))
102            .then(whitespace().repeated().at_least(1))
103            .then(just("week"))
104            .ignored())
105        .map(|()| Interval::Weekday(chrono::Weekday::Fri));
106    let saturday = just("saturday")
107        .ignored()
108        .or(just("sat").ignored())
109        .or(just("3rd")
110            .then(whitespace().repeated().at_least(1))
111            .then(just("day"))
112            .then(whitespace().repeated().at_least(1))
113            .then(just("of"))
114            .then(whitespace().repeated().at_least(1))
115            .then(just("week"))
116            .ignored())
117        .map(|()| Interval::Weekday(chrono::Weekday::Sat));
118    let sunday = just("sunday")
119        .ignored()
120        .or(just("sun").ignored())
121        .or(just("3rd")
122            .then(whitespace().repeated().at_least(1))
123            .then(just("day"))
124            .then(whitespace().repeated().at_least(1))
125            .then(just("of"))
126            .then(whitespace().repeated().at_least(1))
127            .then(just("week"))
128            .ignored())
129        .map(|()| Interval::Weekday(chrono::Weekday::Sun));
130    just("every")
131        .then(whitespace().repeated().at_least(1))
132        .ignore_then(
133            monday
134                .or(tuesday)
135                .or(wednesday)
136                .or(thursday)
137                .or(friday)
138                .or(saturday)
139                .or(sunday),
140        )
141}
142
143fn every<'a>() -> impl Parser<'a, &'a str, Interval, extra::Full<Rich<'a, char>, State, ()>> {
144    let every = just("every")
145        .then(whitespace().repeated().at_least(1))
146        .ignore_then(choice([
147            just("day").to(Interval::NthDay(1)),
148            just("week").to(Interval::NthWeek(1)),
149            just("month").to(Interval::NthMonth(1)),
150            just("quarter").to(Interval::NthQuarter(1)),
151            just("year").to(Interval::NthYear(1)),
152        ]));
153    let every_n_days = just("every")
154        .then(whitespace().repeated().at_least(1))
155        .ignore_then(text::int(10).from_str::<u32>().unwrapped())
156        .then_ignore(whitespace().repeated().at_least(1))
157        .then_ignore(just("days"))
158        .map(Interval::NthDay);
159    let every_n_weeks = just("every")
160        .then(whitespace().repeated().at_least(1))
161        .ignore_then(text::int(10).from_str::<u32>().unwrapped())
162        .then_ignore(whitespace().repeated().at_least(1))
163        .then_ignore(just("weeks"))
164        .map(Interval::NthWeek);
165    let every_n_months = just("every")
166        .then(whitespace().repeated().at_least(1))
167        .ignore_then(text::int(10).from_str::<u32>().unwrapped())
168        .then_ignore(whitespace().repeated().at_least(1))
169        .then_ignore(just("months"))
170        .map(Interval::NthMonth);
171    let every_n_quarterd = just("every")
172        .then(whitespace().repeated().at_least(1))
173        .ignore_then(text::int(10).from_str::<u32>().unwrapped())
174        .then_ignore(whitespace().repeated().at_least(1))
175        .then_ignore(just("quarters"))
176        .map(Interval::NthQuarter);
177    let every_n_years = just("every")
178        .then(whitespace().repeated().at_least(1))
179        .ignore_then(text::int(10).from_str::<u32>().unwrapped())
180        .then_ignore(whitespace().repeated().at_least(1))
181        .then_ignore(just("years"))
182        .map(Interval::NthYear);
183    let every_n = every_n_days
184        .or(every_n_weeks)
185        .or(every_n_months)
186        .or(every_n_quarterd)
187        .or(every_n_years);
188    every.or(every_n)
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn every_day() {
197        let result = interval()
198            .then_ignore(end())
199            .parse("every  day")
200            .into_result();
201        assert_eq!(result, Ok(Interval::NthDay(1)));
202    }
203
204    #[test]
205    fn every_n_day() {
206        let result = interval()
207            .then_ignore(end())
208            .parse("every 3 days")
209            .into_result();
210        assert_eq!(result, Ok(Interval::NthDay(3)));
211    }
212
213    #[test]
214    fn every_week() {
215        let result = interval()
216            .then_ignore(end())
217            .parse("every week")
218            .into_result();
219        assert_eq!(result, Ok(Interval::NthWeek(1)));
220    }
221
222    #[test]
223    fn every_n_week() {
224        let result = interval()
225            .then_ignore(end())
226            .parse("every 4 weeks")
227            .into_result();
228        assert_eq!(result, Ok(Interval::NthWeek(4)));
229    }
230
231    #[test]
232    fn every_month() {
233        let result = interval()
234            .then_ignore(end())
235            .parse("every month")
236            .into_result();
237        assert_eq!(result, Ok(Interval::NthMonth(1)));
238    }
239
240    #[test]
241    fn every_n_month() {
242        let result = interval()
243            .then_ignore(end())
244            .parse("every 2 months")
245            .into_result();
246        assert_eq!(result, Ok(Interval::NthMonth(2)));
247    }
248
249    #[test]
250    fn every_quarter() {
251        let result = interval()
252            .then_ignore(end())
253            .parse("every quarter")
254            .into_result();
255        assert_eq!(result, Ok(Interval::NthQuarter(1)));
256    }
257
258    #[test]
259    fn every_n_quarter() {
260        let result = interval()
261            .then_ignore(end())
262            .parse("every 2 quarters")
263            .into_result();
264        assert_eq!(result, Ok(Interval::NthQuarter(2)));
265    }
266
267    #[test]
268    fn every_year() {
269        let result = interval()
270            .then_ignore(end())
271            .parse("every year")
272            .into_result();
273        assert_eq!(result, Ok(Interval::NthYear(1)));
274    }
275
276    #[test]
277    fn every_n_years() {
278        let result = interval()
279            .then_ignore(end())
280            .parse("every 10 years")
281            .into_result();
282        assert_eq!(result, Ok(Interval::NthYear(10)));
283    }
284
285    #[test]
286    fn every_weekday() {
287        let result = interval()
288            .then_ignore(end())
289            .parse("every tue")
290            .into_result();
291        assert_eq!(result, Ok(Interval::Weekday(chrono::Weekday::Tue)));
292    }
293}