hledger_parser/component/
period.rs

1#![allow(dead_code)]
2
3pub mod interval;
4
5use std::time::SystemTime;
6
7use chrono::Datelike;
8use chumsky::prelude::*;
9
10use crate::component::date::smart::date;
11use crate::component::period::interval::{interval, Interval};
12use crate::component::whitespace::whitespace;
13use crate::state::State;
14
15#[derive(Debug, Clone, PartialEq)]
16pub struct Period {
17    pub interval: Option<Interval>,
18    pub begin: Option<chrono::NaiveDate>,
19    pub end: Option<chrono::NaiveDate>,
20}
21
22pub fn period<'a>() -> impl Parser<'a, &'a str, Period, extra::Full<Rich<'a, char>, State, ()>> {
23    let interval_begin_end = interval()
24        .then_ignore(
25            whitespace()
26                .repeated()
27                .at_least(1)
28                .ignore_then(just("in"))
29                .ignore_then(whitespace().repeated().at_least(1))
30                .or(whitespace().repeated().at_least(1)),
31        )
32        .then(
33            quarter()
34                .or(year_quarter())
35                .or(begin_end())
36                .or(just_end())
37                .or(begin())
38                .or(year_month_day())
39                .or(year_month())
40                .or(year()),
41        )
42        .map(|(interval, (begin, end))| Period {
43            interval: Some(interval),
44            begin,
45            end,
46        });
47
48    let interval = interval().map(|interval| Period {
49        interval: Some(interval),
50        begin: None,
51        end: None,
52    });
53
54    let begin_end = quarter()
55        .or(year_quarter())
56        .or(begin_end())
57        .or(just_end())
58        .or(begin())
59        .or(year_month_day())
60        .or(year_month())
61        .or(year())
62        .map(|(begin, end)| Period {
63            interval: None,
64            begin,
65            end,
66        });
67
68    interval_begin_end.or(interval).or(begin_end)
69}
70
71// returns today's date
72fn today() -> chrono::NaiveDate {
73    let current_time = SystemTime::now();
74    let datetime: chrono::DateTime<chrono::Local> = current_time.into();
75    datetime.date_naive()
76}
77
78// 2009Q1
79fn year_quarter<'a>() -> impl Parser<
80    'a,
81    &'a str,
82    (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
83    extra::Full<Rich<'a, char>, State, ()>,
84> {
85    any()
86        .filter(|c: &char| c.is_ascii_digit())
87        .repeated()
88        .exactly(4)
89        .collect::<String>()
90        .from_str::<i32>()
91        .unwrapped()
92        .then(
93            one_of("qQ")
94                .ignore_then(one_of("1234"))
95                .map(|s: char| s.to_string())
96                .from_str::<u32>()
97                .unwrapped(),
98        )
99        .map(|(year, q)| {
100            (
101                chrono::NaiveDate::from_ymd_opt(year, (q - 1) * 3 + 1, 1),
102                chrono::NaiveDate::from_ymd_opt(year, (q - 1) * 3 + 4, 1),
103            )
104        })
105}
106
107// q1
108fn quarter<'a>() -> impl Parser<
109    'a,
110    &'a str,
111    (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
112    extra::Full<Rich<'a, char>, State, ()>,
113> {
114    one_of("qQ")
115        .ignore_then(one_of("1234"))
116        .map(|s: char| s.to_string())
117        .from_str::<u32>()
118        .unwrapped()
119        .map(|q| {
120            (
121                today().with_month((q - 1) * 3 + 1).unwrap().with_day(1),
122                today().with_month((q - 1) * 3 + 4).unwrap().with_day(1),
123            )
124        })
125}
126
127fn begin_end<'a>() -> impl Parser<
128    'a,
129    &'a str,
130    (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
131    extra::Full<Rich<'a, char>, State, ()>,
132> {
133    just("from")
134        .or(just("since"))
135        .ignore_then(whitespace().repeated().at_least(1))
136        .or_not()
137        .ignore_then(date())
138        .then_ignore(
139            whitespace()
140                .repeated()
141                .then(just("to").or(just("..")).or(just("-")))
142                .or_not(),
143        )
144        .then_ignore(whitespace().repeated())
145        .then(date())
146        .map(|(begin, end)| (Some(begin), Some(end)))
147}
148
149// to 2009
150fn just_end<'a>() -> impl Parser<
151    'a,
152    &'a str,
153    (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
154    extra::Full<Rich<'a, char>, State, ()>,
155> {
156    just("to")
157        .then(whitespace().repeated())
158        .ignore_then(date())
159        .map(|end| (None, Some(end)))
160}
161
162// since 2009
163fn begin<'a>() -> impl Parser<
164    'a,
165    &'a str,
166    (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
167    extra::Full<Rich<'a, char>, State, ()>,
168> {
169    just("from")
170        .or(just("since"))
171        .ignore_then(whitespace().repeated().at_least(1))
172        .ignore_then(date())
173        .map(|begin| (Some(begin), None))
174}
175
176// 2009
177fn year<'a>() -> impl Parser<
178    'a,
179    &'a str,
180    (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
181    extra::Full<Rich<'a, char>, State, ()>,
182> {
183    any()
184        .filter(|c: &char| c.is_ascii_digit())
185        .repeated()
186        .exactly(4)
187        .collect::<String>()
188        .from_str::<i32>()
189        .unwrapped()
190        .map(|year| {
191            (
192                chrono::NaiveDate::from_ymd_opt(year, 1, 1),
193                chrono::NaiveDate::from_ymd_opt(year + 1, 1, 1),
194            )
195        })
196}
197
198// 2009/1
199fn year_month<'a>() -> impl Parser<
200    'a,
201    &'a str,
202    (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
203    extra::Full<Rich<'a, char>, State, ()>,
204> {
205    any()
206        .filter(|c: &char| c.is_ascii_digit())
207        .repeated()
208        .exactly(4)
209        .collect::<String>()
210        .from_str::<i32>()
211        .unwrapped()
212        .then_ignore(one_of("-/."))
213        .then(
214            any()
215                .filter(|c: &char| c.is_ascii_digit())
216                .repeated()
217                .at_least(1)
218                .at_most(2)
219                .collect::<String>()
220                .from_str::<u32>()
221                .unwrapped(),
222        )
223        .map(|(year, month)| {
224            (
225                chrono::NaiveDate::from_ymd_opt(year, month, 1),
226                chrono::NaiveDate::from_ymd_opt(year, month + 1, 1),
227            )
228        })
229}
230
231// 2024/10/1: exact date
232fn year_month_day<'a>() -> impl Parser<
233    'a,
234    &'a str,
235    (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
236    extra::Full<Rich<'a, char>, State, ()>,
237> {
238    let year_month_day = |sep: char| {
239        any()
240            .filter(|c: &char| c.is_ascii_digit())
241            .repeated()
242            .at_least(1)
243            .at_most(4)
244            .collect::<String>()
245            .from_str::<i32>()
246            .unwrapped()
247            .then_ignore(just(sep))
248            .then(
249                any()
250                    .filter(|c: &char| c.is_ascii_digit())
251                    .repeated()
252                    .at_least(1)
253                    .at_most(2)
254                    .collect::<String>()
255                    .from_str::<u32>()
256                    .unwrapped(),
257            )
258            .then_ignore(just(sep))
259            .then(
260                any()
261                    .filter(|c: &char| c.is_ascii_digit())
262                    .repeated()
263                    .at_least(1)
264                    .at_most(2)
265                    .collect::<String>()
266                    .from_str::<u32>()
267                    .unwrapped(),
268            )
269            .map(|((year, month), day)| {
270                (
271                    chrono::NaiveDate::from_ymd_opt(year, month, day),
272                    chrono::NaiveDate::from_ymd_opt(year, month, day + 1),
273                )
274            })
275    };
276    year_month_day('.')
277        .or(year_month_day('/'))
278        .or(year_month_day('-'))
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn from_to() {
287        let result = period()
288            .then_ignore(end())
289            .parse("from 2009/1/1 to 2009/4/1")
290            .into_result();
291        assert_eq!(
292            result,
293            Ok(Period {
294                interval: None,
295                begin: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
296                end: Some(chrono::NaiveDate::from_ymd_opt(2009, 4, 1).unwrap()),
297            })
298        );
299    }
300
301    #[test]
302    fn dots() {
303        let result = period()
304            .then_ignore(end())
305            .parse("2009/1/1..2009/4/1")
306            .into_result();
307        assert_eq!(
308            result,
309            Ok(Period {
310                interval: None,
311                begin: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
312                end: Some(chrono::NaiveDate::from_ymd_opt(2009, 4, 1).unwrap()),
313            })
314        );
315    }
316
317    #[test]
318    fn only_begin() {
319        let result = period()
320            .then_ignore(end())
321            .parse("since 2009/1")
322            .into_result();
323        assert_eq!(
324            result,
325            Ok(Period {
326                interval: None,
327                begin: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
328                end: None,
329            })
330        );
331    }
332
333    #[test]
334    fn only_end() {
335        let result = period().then_ignore(end()).parse("to 2009").into_result();
336        assert_eq!(
337            result,
338            Ok(Period {
339                interval: None,
340                begin: None,
341                end: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
342            })
343        );
344    }
345
346    #[test]
347    fn year() {
348        let result = period().then_ignore(end()).parse("2009").into_result();
349        assert_eq!(
350            result,
351            Ok(Period {
352                interval: None,
353                begin: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
354                end: Some(chrono::NaiveDate::from_ymd_opt(2010, 1, 1).unwrap()),
355            })
356        );
357    }
358
359    #[test]
360    fn month() {
361        let result = period().then_ignore(end()).parse("2009/1").into_result();
362        assert_eq!(
363            result,
364            Ok(Period {
365                interval: None,
366                begin: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
367                end: Some(chrono::NaiveDate::from_ymd_opt(2009, 2, 1).unwrap()),
368            })
369        );
370    }
371
372    #[test]
373    fn day() {
374        let result = period().then_ignore(end()).parse("2009/1/1").into_result();
375        assert_eq!(
376            result,
377            Ok(Period {
378                interval: None,
379                begin: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
380                end: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 2).unwrap()),
381            })
382        );
383    }
384
385    #[test]
386    fn quarter() {
387        let result = period().then_ignore(end()).parse("q3").into_result();
388        assert_eq!(
389            result,
390            Ok(Period {
391                interval: None,
392                begin: today().with_month(7).unwrap().with_day(1),
393                end: today().with_month(10).unwrap().with_day(1),
394            })
395        );
396    }
397
398    #[test]
399    fn year_quarter() {
400        let result = period().then_ignore(end()).parse("2009Q3").into_result();
401        assert_eq!(
402            result,
403            Ok(Period {
404                interval: None,
405                begin: chrono::NaiveDate::from_ymd_opt(2009, 7, 1),
406                end: chrono::NaiveDate::from_ymd_opt(2009, 10, 1),
407            })
408        );
409    }
410
411    #[test]
412    fn with_in_interval() {
413        let result = period()
414            .then_ignore(end())
415            .parse("every 2 weeks in 2008")
416            .into_result();
417        assert_eq!(
418            result,
419            Ok(Period {
420                interval: Some(Interval::NthWeek(2)),
421                begin: chrono::NaiveDate::from_ymd_opt(2008, 1, 1),
422                end: chrono::NaiveDate::from_ymd_opt(2009, 1, 1),
423            })
424        );
425    }
426
427    #[test]
428    fn with_interval() {
429        let result = period()
430            .then_ignore(end())
431            .parse("weekly from 2009/1/1 to 2009/4/1")
432            .into_result();
433        assert_eq!(
434            result,
435            Ok(Period {
436                interval: Some(Interval::NthWeek(1)),
437                begin: chrono::NaiveDate::from_ymd_opt(2009, 1, 1),
438                end: chrono::NaiveDate::from_ymd_opt(2009, 4, 1),
439            })
440        );
441    }
442
443    #[test]
444    fn just_interval() {
445        let result = period().then_ignore(end()).parse("monthly").into_result();
446        assert_eq!(
447            result,
448            Ok(Period {
449                interval: Some(Interval::NthMonth(1)),
450                begin: None,
451                end: None,
452            })
453        );
454    }
455}