• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ngalaiko / hledger-desktop / 11782383692

11 Nov 2024 04:25PM UTC coverage: 68.111%. Remained the same
11782383692

push

github

ngalaiko
hledger-desktop: offload parsing to rayon

0 of 5 new or added lines in 1 file covered. (0.0%)

20 existing lines in 1 file now uncovered.

786 of 1154 relevant lines covered (68.11%)

2.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.8
/crates/hledger-parser/src/component/date/smart.rs
1
use std::time::SystemTime;
2

3
use chrono::Datelike;
4
use chumsky::prelude::*;
5

6
use crate::{component::whitespace::whitespace, state::State};
7

8
pub fn date<'a>(
1✔
9
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
10
    periods_ahead()
21✔
11
        .or(periods_ago())
1✔
12
        .or(n_periods())
1✔
13
        .or(rel_word_period())
1✔
14
        .or(words())
1✔
15
        .or(rel_word_period())
1✔
16
        .or(month_day())
1✔
17
        .or(year_month_day())
1✔
18
        .or(start_of_month_numeric())
1✔
19
        .or(eight_digits())
1✔
20
        .or(six_digits())
1✔
21
        .or(four_digits())
1✔
22
        .or(just_day())
3✔
23
        .or(month_name())
3✔
24
}
25

26
#[derive(Debug, Clone)]
27
enum Period {
28
    Day,
29
    Week,
30
    Month,
31
    Quarter,
32
    Year,
33
}
34

35
fn period<'a>() -> impl Parser<'a, &'a str, Period, extra::Full<Rich<'a, char>, State, ()>> {
1✔
36
    choice([
2✔
37
        just("day").to(Period::Day),
1✔
38
        just("week").to(Period::Week),
1✔
39
        just("month").to(Period::Month),
1✔
40
        just("quarter").to(Period::Quarter),
1✔
41
        just("year").to(Period::Year),
1✔
42
    ])
43
    .then_ignore(just("s").or_not())
1✔
44
}
45

46
// n days/weeks/months/quarters/years ago : -n periods from the current period
47
fn periods_ago<'a>(
1✔
48
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
49
    text::int(10)
5✔
50
        .then_ignore(whitespace().repeated().at_least(1))
1✔
51
        .then(period())
1✔
52
        .then_ignore(whitespace().repeated().at_least(1))
1✔
53
        .then_ignore(just("ago"))
1✔
54
        .try_map(|(length, period), span| {
2✔
55
            match period {
2✔
56
                Period::Day => {
×
57
                    today().checked_sub_days(chrono::Days::new(length.parse::<u64>().unwrap()))
×
58
                }
59
                Period::Week => {
×
60
                    today().checked_sub_days(chrono::Days::new(length.parse::<u64>().unwrap() * 7))
×
61
                }
62
                Period::Month => {
×
63
                    today().checked_sub_months(chrono::Months::new(length.parse::<u32>().unwrap()))
2✔
64
                }
65
                Period::Quarter => today()
×
66
                    .checked_sub_months(chrono::Months::new(length.parse::<u32>().unwrap() * 3)),
×
67
                Period::Year => today()
×
68
                    .checked_sub_months(chrono::Months::new(length.parse::<u32>().unwrap() * 12)),
×
69
            }
70
            .ok_or(Rich::custom(span, "not a valid date"))
1✔
71
        })
72
}
73

74
// n days/weeks/months/quarters/years ahead : n periods from the current period
75
fn periods_ahead<'a>(
1✔
76
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
77
    text::int(10)
5✔
78
        .then_ignore(whitespace().repeated().at_least(1))
1✔
79
        .then(period())
1✔
80
        .then_ignore(whitespace().repeated().at_least(1))
1✔
81
        .then_ignore(just("ahead"))
1✔
82
        .try_map(|(length, period), span| {
2✔
83
            match period {
2✔
84
                Period::Day => {
×
85
                    today().checked_add_days(chrono::Days::new(length.parse::<u64>().unwrap()))
×
86
                }
87
                Period::Week => {
×
88
                    today().checked_add_days(chrono::Days::new(length.parse::<u64>().unwrap() * 7))
3✔
89
                }
90
                Period::Month => {
×
91
                    today().checked_add_months(chrono::Months::new(length.parse::<u32>().unwrap()))
×
92
                }
93
                Period::Quarter => today()
×
94
                    .checked_add_months(chrono::Months::new(length.parse::<u32>().unwrap() * 3)),
×
95
                Period::Year => today()
×
96
                    .checked_add_months(chrono::Months::new(length.parse::<u32>().unwrap() * 12)),
×
97
            }
98
            .ok_or(Rich::custom(span, "not a valid date"))
1✔
99
        })
100
}
101

102
// in n days/weeks/months/quarters/years : n periods from the current period
103
fn n_periods<'a>(
1✔
104
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
105
    just("in")
5✔
106
        .ignore_then(whitespace().repeated().at_least(1))
1✔
107
        .ignore_then(text::int(10))
1✔
108
        .then_ignore(whitespace().repeated().at_least(1))
1✔
109
        .then(period())
1✔
110
        .try_map(|(length, period), span| {
2✔
111
            match period {
2✔
112
                Period::Day => {
×
113
                    today().checked_add_days(chrono::Days::new(length.parse::<u64>().unwrap()))
2✔
114
                }
115
                Period::Week => {
×
116
                    today().checked_add_days(chrono::Days::new(length.parse::<u64>().unwrap() * 7))
×
117
                }
118
                Period::Month => {
×
119
                    today().checked_add_months(chrono::Months::new(length.parse::<u32>().unwrap()))
×
120
                }
121
                Period::Quarter => today()
×
122
                    .checked_add_months(chrono::Months::new(length.parse::<u32>().unwrap() * 3)),
×
123
                Period::Year => today()
×
124
                    .checked_add_months(chrono::Months::new(length.parse::<u32>().unwrap() * 12)),
×
125
            }
126
            .ok_or(Rich::custom(span, "not a valid date"))
1✔
127
        })
128
}
129

130
// last/this/next day/week/month/quarter/year : -1, 0, 1 periods from the current period
131
fn rel_word_period<'a>(
1✔
132
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
133
    choice([just("last").to(-1), just("this").to(0), just("next").to(1)])
3✔
134
        .then_ignore(whitespace().repeated().at_least(1))
1✔
135
        .then(period())
1✔
136
        .try_map(|(rel, period), span| {
2✔
137
            match period {
2✔
138
                Period::Day => {
×
139
                    if rel >= 0 {
×
140
                        today().checked_add_days(chrono::Days::new(1))
×
141
                    } else {
142
                        today().checked_sub_days(chrono::Days::new(1))
×
143
                    }
144
                }
145
                Period::Week => {
×
146
                    if rel >= 0 {
1✔
147
                        today().checked_add_days(chrono::Days::new(7))
×
148
                    } else {
149
                        today().checked_sub_days(chrono::Days::new(7))
1✔
150
                    }
151
                }
152
                Period::Month => {
×
153
                    if rel >= 0 {
×
154
                        today().checked_add_months(chrono::Months::new(1))
×
155
                    } else {
156
                        today().checked_sub_months(chrono::Months::new(1))
×
157
                    }
158
                }
159
                Period::Quarter => {
×
160
                    if rel >= 0 {
×
161
                        today().checked_add_months(chrono::Months::new(3))
×
162
                    } else {
163
                        today().checked_sub_months(chrono::Months::new(3))
×
164
                    }
165
                }
166
                Period::Year => {
×
167
                    if rel >= 0 {
1✔
168
                        today().checked_add_months(chrono::Months::new(12))
1✔
169
                    } else {
170
                        today().checked_sub_months(chrono::Months::new(12))
×
171
                    }
172
                }
173
            }
174
            .ok_or(Rich::custom(span, "not a valid date"))
1✔
175
        })
176
}
177

178
// yesterday, today, tomorrow : -1, 0, 1 days from today
179
fn words<'a>() -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>>
1✔
180
{
181
    choice([
1✔
182
        just("yesterday").to(today().checked_sub_days(chrono::Days::new(1)).unwrap()),
2✔
183
        just("today").to(today()),
1✔
184
        just("tomorrow").to(today().checked_add_days(chrono::Days::new(1)).unwrap()),
2✔
185
    ])
186
}
187

188
// 2024/10/1: exact date
189
fn year_month_day<'a>(
1✔
190
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
191
    let year_month_day = |sep: char| {
1✔
192
        any()
5✔
193
            .filter(|c: &char| c.is_ascii_digit())
6✔
194
            .repeated()
195
            .at_least(1)
196
            .at_most(4)
197
            .collect::<String>()
198
            .from_str::<i32>()
199
            .unwrapped()
200
            .then_ignore(just(sep))
1✔
201
            .then(
202
                any()
1✔
203
                    .filter(|c: &char| c.is_ascii_digit())
2✔
204
                    .repeated()
×
205
                    .at_least(1)
×
206
                    .at_most(2)
×
207
                    .collect::<String>()
×
208
                    .from_str::<u32>()
×
209
                    .unwrapped(),
×
210
            )
211
            .then_ignore(just(sep))
1✔
212
            .then(
213
                any()
1✔
214
                    .filter(|c: &char| c.is_ascii_digit())
4✔
215
                    .repeated()
×
216
                    .at_least(1)
×
217
                    .at_most(2)
×
218
                    .collect::<String>()
×
219
                    .from_str::<u32>()
×
220
                    .unwrapped(),
×
221
            )
222
            .try_map(|((year, month), day), span| {
4✔
223
                chrono::NaiveDate::from_ymd_opt(year, month, day).ok_or(Rich::custom(
4✔
224
                    span,
×
225
                    format!("{year}-{month}-{day} is not a valid date"),
2✔
226
                ))
227
            })
228
    };
229
    year_month_day('.')
3✔
230
        .or(year_month_day('/'))
1✔
231
        .or(year_month_day('-'))
1✔
232
}
233

234
// 10/1: October 1st in current year
235
fn month_day<'a>(
1✔
236
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
237
    let month_day = |sep: char| {
1✔
238
        any()
3✔
239
            .filter(|c: &char| c.is_ascii_digit())
6✔
240
            .repeated()
241
            .at_least(1)
242
            .at_most(2)
243
            .collect::<String>()
244
            .from_str::<u32>()
245
            .unwrapped()
246
            .then_ignore(just(sep))
1✔
247
            .then(
248
                any()
1✔
249
                    .filter(|c: &char| c.is_ascii_digit())
4✔
250
                    .repeated()
×
251
                    .at_least(1)
×
252
                    .at_most(2)
×
253
                    .collect::<String>()
×
254
                    .from_str::<u32>()
×
255
                    .unwrapped(),
×
256
            )
257
            .map_with(|(month, day), e| {
4✔
258
                let state: &mut State = e.state();
2✔
259
                chrono::NaiveDate::from_ymd_opt(state.year, month, day)
2✔
260
            })
261
            .try_map(|date, span| date.ok_or(Rich::custom(span, "not a valid date")))
4✔
262
    };
263
    month_day('.').or(month_day('/')).or(month_day('-'))
1✔
264
}
265

266
// 2004: start of year
267
fn four_digits<'a>(
1✔
268
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
269
    any()
3✔
270
        .filter(|c: &char| c.is_ascii_digit())
2✔
271
        .repeated()
272
        .exactly(4)
273
        .collect::<String>()
274
        .from_str::<i32>()
275
        .unwrapped()
276
        .try_map(|year, span| {
1✔
277
            chrono::NaiveDate::from_ymd_opt(year, 1, 1).ok_or(Rich::custom(
2✔
278
                span,
×
279
                format!("{year}-01-01 is not a valid date"),
1✔
280
            ))
281
        })
282
}
283

284
// 2004-10: start of month
285
fn start_of_month_numeric<'a>(
1✔
286
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
287
    any()
3✔
288
        .filter(|c: &char| c.is_ascii_digit())
6✔
289
        .repeated()
290
        .exactly(4)
291
        .collect::<String>()
292
        .from_str::<i32>()
293
        .unwrapped()
294
        .then_ignore(one_of("-/."))
1✔
295
        .then(
296
            any()
1✔
297
                .filter(|c: &char| c.is_ascii_digit())
3✔
298
                .repeated()
×
299
                .at_least(1)
×
300
                .at_most(2)
×
301
                .collect::<String>()
×
302
                .from_str::<u32>()
×
303
                .unwrapped(),
×
304
        )
305
        .try_map(|(year, month), span| {
4✔
306
            chrono::NaiveDate::from_ymd_opt(year, month, 1).ok_or(Rich::custom(
4✔
307
                span,
×
308
                format!("{year}-{month}-01 is not a valid date"),
2✔
309
            ))
310
        })
311
}
312

313
// 6 digit YYYYMM with valid year and month
314
fn six_digits<'a>(
1✔
315
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
316
    any()
1✔
317
        .filter(|c: &char| c.is_ascii_digit())
2✔
318
        .repeated()
319
        .exactly(6)
320
        .collect::<String>()
321
        .try_map(|yearmonth, span| {
1✔
322
            let year = yearmonth[0..4]
2✔
323
                .parse::<i32>()
×
324
                .map_err(|error| Rich::custom(span, error))?;
×
325
            let month = yearmonth[4..6]
2✔
326
                .parse::<u32>()
×
327
                .map_err(|error| Rich::custom(span, error))?;
×
328
            chrono::NaiveDate::from_ymd_opt(year, month, 1).ok_or(Rich::custom(
2✔
329
                span,
×
330
                format!("{year}-{month}-01 is not a valid date"),
1✔
331
            ))
332
        })
333
}
334

335
// 21: 21st day in current month
336
fn just_day<'a>(
3✔
337
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
338
    any()
3✔
339
        .filter(|c: &char| c.is_ascii_digit())
4✔
340
        .repeated()
341
        .exactly(2)
342
        .collect::<String>()
343
        .from_str::<u32>()
344
        .unwrapped()
345
        .try_map(|day, span| {
2✔
346
            if let Some(date) = today().with_day(day) {
3✔
347
                Ok(date)
1✔
348
            } else {
349
                Err(Rich::custom(
1✔
350
                    span,
×
351
                    format!("{day} day does not exist in the current month"),
1✔
352
                ))
353
            }
354
        })
355
}
356

357
// returns today's date
358
fn today() -> chrono::NaiveDate {
1✔
359
    let current_time = SystemTime::now();
1✔
360
    let datetime: chrono::DateTime<chrono::Local> = current_time.into();
1✔
361
    datetime.date_naive()
1✔
362
}
363

364
// oct or october: October 1st in current year
365
fn month_name<'a>(
3✔
366
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
367
    let start_of_month = |m: u32| today().with_day(1).unwrap().with_month(m).unwrap();
15✔
368
    choice([
4✔
369
        just("january").to(start_of_month(1)),
3✔
370
        just("jan").to(start_of_month(1)),
4✔
371
        just("february").to(start_of_month(2)),
4✔
372
        just("feb").to(start_of_month(2)),
4✔
373
        just("march").to(start_of_month(3)),
4✔
374
        just("mar").to(start_of_month(3)),
4✔
375
        just("april").to(start_of_month(4)),
4✔
376
        just("apr").to(start_of_month(4)),
4✔
377
        just("may").to(start_of_month(5)),
4✔
378
        just("june").to(start_of_month(6)),
4✔
379
        just("jun").to(start_of_month(6)),
4✔
380
        just("july").to(start_of_month(7)),
4✔
381
        just("jul").to(start_of_month(7)),
4✔
382
        just("august").to(start_of_month(8)),
4✔
383
        just("aug").to(start_of_month(8)),
4✔
384
        just("september").to(start_of_month(9)),
4✔
385
        just("sep").to(start_of_month(9)),
4✔
386
        just("october").to(start_of_month(10)),
4✔
387
        just("oct").to(start_of_month(10)),
4✔
388
        just("november").to(start_of_month(11)),
4✔
389
        just("nov").to(start_of_month(11)),
4✔
390
        just("december").to(start_of_month(12)),
4✔
391
        just("dec").to(start_of_month(12)),
4✔
392
    ])
393
}
394

395
// 8 digit YYYYMMDD with valid year month and day
396
fn eight_digits<'a>(
1✔
397
) -> impl Parser<'a, &'a str, chrono::NaiveDate, extra::Full<Rich<'a, char>, State, ()>> {
398
    any()
1✔
399
        .filter(|c: &char| c.is_ascii_digit())
6✔
400
        .repeated()
401
        .exactly(8)
402
        .collect::<String>()
403
        .try_map(|yearmonthday, span| {
2✔
404
            let year = yearmonthday[0..4]
4✔
405
                .parse::<i32>()
×
406
                .map_err(|error| Rich::custom(span, error))?;
×
407
            let month = yearmonthday[4..6]
4✔
408
                .parse::<u32>()
×
409
                .map_err(|error| Rich::custom(span, error))?;
×
410
            let day = yearmonthday[6..8]
4✔
411
                .parse::<u32>()
×
412
                .map_err(|error| Rich::custom(span, error))?;
×
413
            chrono::NaiveDate::from_ymd_opt(year, month, day).ok_or(Rich::custom(
4✔
414
                span,
×
415
                format!("{year}-{month}-{day} is not a valid date"),
2✔
416
            ))
417
        })
418
}
419

420
#[cfg(test)]
421
mod tests {
422
    use chumsky::prelude::end;
423

424
    use super::*;
425

426
    #[test]
427
    fn start_of_month_numeric() {
428
        let result = date().then_ignore(end()).parse("2024-03").into_result();
429
        assert_eq!(
430
            result,
431
            Ok(chrono::NaiveDate::from_ymd_opt(2024, 3, 1).unwrap()),
432
        );
433
    }
434

435
    #[test]
436
    fn start_of_month_numeric_err() {
437
        let result = date().then_ignore(end()).parse("2024-31").into_result();
438
        assert!(result.is_err());
439
    }
440

441
    #[test]
442
    fn four_digits() {
443
        let result = date().then_ignore(end()).parse("2024").into_result();
444
        assert_eq!(
445
            result,
446
            Ok(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
447
        );
448
    }
449

450
    #[test]
451
    fn eight_digits() {
452
        let result = date().then_ignore(end()).parse("20240112").into_result();
453
        assert_eq!(
454
            result,
455
            Ok(chrono::NaiveDate::from_ymd_opt(2024, 1, 12).unwrap()),
456
        );
457
    }
458

459
    #[test]
460
    fn eightsix_digits_err() {
461
        let result = date().then_ignore(end()).parse("20240132").into_result();
462
        assert!(result.is_err());
463
    }
464

465
    #[test]
466
    fn six_digits() {
467
        let result = date().then_ignore(end()).parse("202401").into_result();
468
        assert_eq!(
469
            result,
470
            Ok(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
471
        );
472
    }
473

474
    #[test]
475
    fn six_digits_err() {
476
        let result = date().then_ignore(end()).parse("202431").into_result();
477
        assert!(result.is_err());
478
    }
479

480
    #[test]
481
    fn year_month_day() {
482
        let result = date().then_ignore(end()).parse("2018.10.1").into_result();
483
        assert_eq!(
484
            result,
485
            Ok(chrono::NaiveDate::from_ymd_opt(2018, 10, 1).unwrap())
486
        );
487
    }
488

489
    #[test]
490
    fn year_month_day_err() {
491
        let result = date().then_ignore(end()).parse("2023/13/1").into_result();
492
        assert!(result.is_err());
493
    }
494

495
    #[test]
496
    fn month_day() {
497
        let result = date().then_ignore(end()).parse("10/1").into_result();
498
        assert_eq!(
499
            result,
500
            Ok(today().with_month(10).unwrap().with_day(1).unwrap())
501
        );
502
    }
503

504
    #[test]
505
    fn month_day_err() {
506
        let result = date().then_ignore(end()).parse("13/1").into_result();
507
        assert!(result.is_err());
508
    }
509

510
    #[test]
511
    fn month_name() {
512
        let result = date().then_ignore(end()).parse("october").into_result();
513
        assert_eq!(
514
            result,
515
            Ok(today().with_month(10).unwrap().with_day(1).unwrap())
516
        );
517
    }
518

519
    #[test]
520
    fn in_three_days() {
521
        let result = date().then_ignore(end()).parse("in  3 days").into_result();
522
        assert_eq!(
523
            result,
524
            Ok(today().checked_add_days(chrono::Days::new(3)).unwrap())
525
        );
526
    }
527

528
    #[test]
529
    fn tomorrow() {
530
        let result = date().then_ignore(end()).parse("tomorrow").into_result();
531
        assert_eq!(
532
            result,
533
            Ok(today().checked_add_days(chrono::Days::new(1)).unwrap())
534
        );
535
    }
536

537
    #[test]
538
    fn test_today() {
539
        let result = date().then_ignore(end()).parse("today").into_result();
540
        assert_eq!(result, Ok(today()));
541
    }
542

543
    #[test]
544
    fn month_ago() {
545
        let result = date()
546
            .then_ignore(end())
547
            .parse("2 months  ago")
548
            .into_result();
549
        assert_eq!(
550
            result,
551
            Ok(today().checked_sub_months(chrono::Months::new(2)).unwrap())
552
        );
553
    }
554

555
    #[test]
556
    fn weeks_ahead() {
557
        let result = date()
558
            .then_ignore(end())
559
            .parse("3 weeks  ahead")
560
            .into_result();
561
        assert_eq!(
562
            result,
563
            Ok(today().checked_add_days(chrono::Days::new(21)).unwrap())
564
        );
565
    }
566

567
    #[test]
568
    fn yesterday() {
569
        let result = date().then_ignore(end()).parse("yesterday").into_result();
570
        assert_eq!(
571
            result,
572
            Ok(today().checked_sub_days(chrono::Days::new(1)).unwrap())
573
        );
574
    }
575

576
    #[test]
577
    fn last_week() {
578
        let result = date().then_ignore(end()).parse("last week").into_result();
579
        assert_eq!(
580
            result,
581
            Ok(today().checked_sub_days(chrono::Days::new(7)).unwrap())
582
        );
583
    }
584

585
    #[test]
586
    fn next_year() {
587
        let result = date().then_ignore(end()).parse("next year").into_result();
588
        assert_eq!(
589
            result,
590
            Ok(today().checked_add_months(chrono::Months::new(12)).unwrap())
591
        );
592
    }
593

594
    #[test]
595
    fn just_day() {
596
        let result = date().then_ignore(end()).parse("21").into_result();
597
        assert_eq!(result, Ok(today().with_day(21).unwrap()));
598
    }
599

600
    #[test]
601
    fn just_day_err() {
602
        let result = date().then_ignore(end()).parse("32").into_result();
603
        assert!(result.is_err());
604
    }
605
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc