two_timer/
lib.rs

1/*!
2
3This crate provides a `parse` function to convert English time expressions into a pair
4of timestamps representing a time range. It converts "today" into the first and last
5moments of today, "May 6, 1968" into the first and last moments of that day, "last year"
6into the first and last moments of that year, and so on. It does this even for expressions
7generally interpreted as referring to a point in time, such as "3 PM", though for these it
8always assumes a granularity of one second. For pointwise expression the first moment is the
9point explicitly named. The `parse` expression actually returns a 3-tuple consisting of the
10two timestamps and whether the expression is literally a range -- two time expressions
11separated by a preposition such as "to", "through", "up to", or "until".
12
13# Example
14
15```rust
16extern crate two_timer;
17use two_timer::{parse, Config};
18extern crate chrono;
19use chrono::naive::NaiveDate;
20
21pub fn main() {
22    let phrases = [
23        "now",
24        "this year",
25        "last Friday",
26        "from now to the end of time",
27        "Ragnarok",
28        "at 3:00 pm today",
29        "5/6/69",
30        "Tuesday, May 6, 1969 at 3:52 AM",
31        "March 15, 44 BC",
32        "Friday the 13th",
33        "five minutes before and after midnight",
34    ];
35    // find the maximum phrase length for pretty formatting
36    let max = phrases
37        .iter()
38        .max_by(|a, b| a.len().cmp(&b.len()))
39        .unwrap()
40        .len();
41    for phrase in phrases.iter() {
42        match parse(phrase, None) {
43            Ok((d1, d2, _)) => println!("{:width$} => {} --- {}", phrase, d1, d2, width = max),
44            Err(e) => println!("{:?}", e),
45        }
46    }
47    let now = NaiveDate::from_ymd_opt(1066, 10, 14).unwrap().and_hms(12, 30, 15);
48    println!("\nlet \"now\" be some moment during the Battle of Hastings, specifically {}\n", now);
49    let conf = Config::new().now(now);
50    for phrase in phrases.iter() {
51        match parse(phrase, Some(conf.clone())) {
52            Ok((d1, d2, _)) => println!("{:width$} => {} --- {}", phrase, d1, d2, width = max),
53            Err(e) => println!("{:?}", e),
54        }
55    }
56}
57```
58produces
59```text
60now                                    => 2019-02-03 14:40:00 --- 2019-02-03 14:41:00
61this year                              => 2019-01-01 00:00:00 --- 2020-01-01 00:00:00
62last Friday                            => 2019-01-25 00:00:00 --- 2019-01-26 00:00:00
63from now to the end of time            => 2019-02-03 14:40:00 --- +262143-12-31 23:59:59.999
64Ragnarok                               => +262143-12-31 23:59:59.999 --- +262143-12-31 23:59:59.999
65at 3:00 pm today                       => 2019-02-03 15:00:00 --- 2019-02-03 15:01:00
665/6/69                                 => 1969-05-06 00:00:00 --- 1969-05-07 00:00:00
67Tuesday, May 6, 1969 at 3:52 AM        => 1969-05-06 03:52:00 --- 1969-05-06 03:53:00
68March 15, 44 BC                        => -0043-03-15 00:00:00 --- -0043-03-16 00:00:00
69Friday the 13th                        => 2018-07-13 00:00:00 --- 2018-07-14 00:00:00
70five minutes before and after midnight => 2019-02-02 23:55:00 --- 2019-02-03 00:05:00
71
72let "now" be some moment during the Battle of Hastings, specifically 1066-10-14 12:30:15
73
74now                                    => 1066-10-14 12:30:00 --- 1066-10-14 12:31:00
75this year                              => 1066-01-01 00:00:00 --- 1067-01-01 00:00:00
76last Friday                            => 1066-10-05 00:00:00 --- 1066-10-06 00:00:00
77from now to the end of time            => 1066-10-14 12:30:00 --- +262143-12-31 23:59:59.999
78Ragnarok                               => +262143-12-31 23:59:59.999 --- +262143-12-31 23:59:59.999
79at 3:00 pm today                       => 1066-10-14 15:00:00 --- 1066-10-14 15:01:00
805/6/69                                 => 0969-05-06 00:00:00 --- 0969-05-07 00:00:00
81Tuesday, May 6, 1969 at 3:52 AM        => 1969-05-06 03:52:00 --- 1969-05-06 03:53:00
82March 15, 44 BC                        => -0043-03-15 00:00:00 --- -0043-03-16 00:00:00
83Friday the 13th                        => 1066-07-13 00:00:00 --- 1066-07-14 00:00:00
84five minutes before and after midnight => 1066-10-13 23:55:00 --- 1066-10-14 00:05:00
85```
86
87For the full grammar of time expressions, view the source of the `parse` function and
88scroll up. The grammar is provided at the top of the file.
89
90# Relative Times
91
92It is common in English to use time expressions which must be interpreted relative to some
93context. The context may be verb tense, other events in the discourse, or other semantic or
94pragmatic clues. The `two_timer` `parse` function doesn't attempt to infer context perfectly, but
95it does make some attempt to get the context right. So, for instance "last Monday through Friday", said
96on Saturday, will end on a different day from "next Monday through Friday". The general rules
97are
98
991. a fully-specified expression in a pair will provide the context for the other expression
1002. a relative expression will be interpreted as appropriate given its order -- the second expression
101describes a time after the first
1023. if neither expression is fully-specified, the first will be interpreted relative to "now" and the
103second relative to the first
1044. a moment interpreted relative to "now" will be assumed to be before now unless the configuration
105parameter `default_to_past` is set to `false`, in which case it will be assumed to be after now
106
107The rules of interpretation for relative time expressions in ranges will likely be refined further
108in the future.
109
110# Clock Time
111
112The parse function interprets expressions such as "3:00" as referring to time on a 24 hour clock, so
113"3:00" will be interpreted as "3:00 AM". This is true even in ranges such as "3:00 PM to 4", where the
114more natural interpretation might be "3:00 PM to 4:00 PM".
115
116# Years Near 0
117
118Since it is common to abbreviate years to the last two digits of the century, two-digit
119years will be interpreted as abbreviated unless followed by a suffix such as "B.C.E." or "AD".
120They will be interpreted by default as the the nearest appropriate *previous* year to the current moment,
121so in 2010 "'11" will be interpreted as 1911, not 2011. If you set the configuration parameter
122`default_to_past` to `false` this is reversed, so "'11" in 2020 will be interpreted as 2111.
123
124# The Second Time in Ranges
125
126For single expressions, like "this year", "today", "3:00", or "next month", the second of the
127two timestamps is straightforward -- it is the end of the relevant temporal unit. "1971" will
128be interpreted as the first moment of the first day of 1971 through, but excluding, the first
129moment of the first day of 1972, so the second timestamp will be this first excluded moment.
130
131When the parsed expression describes a range, we're really dealing with two potentially overlapping
132pairs of timestamps and the choice of the terminal timestamp gets trickier. The general rule
133will be that if the second interval is shorter than a day, the first timestamp is the first excluded moment,
134so "today to 3:00 PM" means the first moment of the day up to, but excluding, 3:00 PM. If the second
135unit is as big as or larger than a day, which timestamp is used varies according to the preposition.
136"This week up to Friday" excludes all of Friday. "This week through Friday" includes all of Friday.
137Prepositions are assumed to fall into either the "to" class or the "through" class. You may also use
138a series of dashes as a synonym for "through", so "this week - fri" is equivalent to "this week through Friday".
139For the most current list of prepositions in each class, consult the grammar used for parsing, but
140as of the moment, these are the rules:
141
142```text
143        up_to => [["to", "until", "up to", "till"]]
144        through => [["up through", "through", "thru"]] | r("-+")
145```
146
147# Pay Periods
148
149I'm writing this library in anticipation of, for the sake of amusement, rewriting [JobLog](https://metacpan.org/pod/App::JobLog)
150in Rust. This means I need the time expressions parsed to include pay periods. Pay periods, though,
151are defined relative to some reference date -- a particular Sunday, say -- and have a variable period.
152`two_timer`, and JobLog, assume pay periods are of a fixed length and tile the timeline without overlap, so a
153pay period of a calendrical month is problematic.
154
155If you need to interpret "last pay period", say, you will need to specify when this pay period began, or
156when some pay period began or will begin, and a pay period length in days. The `parse` function has a second
157optional argument, a `Config` object, whose chief function outside of testing is to provide this information. So,
158for example, you could do this:
159
160```rust
161# extern crate two_timer;
162# use two_timer::{parse, Config};
163let (reference_time, _, _) = parse("5/6/69", None).unwrap();
164let config = Config::new().pay_period_start(Some(reference_time.date()));
165let (t1, t2, _) = parse("next pay period", Some(config)).unwrap();
166```
167
168# Ambiguous Year Formats
169
170`two_timer` will try various year-month-day permutations until one of them parses given that days are in the range 1-31 and
171months, 1-12. This is the order in which it tries these permutations:
172
1731. year/month/day
1742. year/day/month
1753. month/day/year
1764. day/month/year
177
178The potential unit separators are `/`, `.`, and `-`. Whitespace is optional.
179
180# Timezones
181
182At the moment `two_timer` only produces "naive" times. Sorry about that.
183
184# Optional Features
185
186The regular expression used by two-timer is extremely efficient once compiled but extremely slow to compile.
187This means that the first use of the regular expression will ocassion a perceptible delay. I wrote two-timer as
188a component of a Rust re-write of a Perl command line application I also wrote, [App::JobLog](https://metacpan.org/pod/distribution/App-JobLog/bin/job).
189Compiling the full time grammar required by two-timer makes the common use cases for the Rust version of the application
190slower than the Perl version. To address this I added an optional feature to two-timer that one can enable like so:
191
192```toml
193[dependencies.two_timer]
194version = "~2.2"
195features = ["small_grammar"]
196```
197
198This will cause two-timer to attempt to parse a time expression initially with a simplified grammar containing
199only the typical expressions used with JobLog, falling back on the full grammar if this fails. These are
200
2011. Days of the week, optionally abbreviated
202   * Tuesday
203   * tue
204   * tu
2052. Month names
206   * June
207   * Jun
2083. Days, months, or fixed periods of time modified by "this" or "last"
209   * this month
210   * last week
211   * this year
212   * this pay period
213   * last Monday
2144. Certain temporal adverbs
215   * now
216   * today
217   * yesterday
218});
219
220*/
221
222#![recursion_limit = "1024"]
223#[macro_use]
224extern crate pidgin;
225#[macro_use]
226extern crate lazy_static;
227extern crate chrono;
228extern crate serde_json;
229use chrono::naive::{NaiveDate, NaiveDateTime};
230use chrono::{Datelike, Duration, Local, Timelike, Weekday};
231use pidgin::{Grammar, Match, Matcher};
232use regex::Regex;
233
234lazy_static! {
235    // making this public is useful for testing, but best to keep it hidden to
236    // limit complexity and commitment
237    #[doc(hidden)]
238    pub static ref GRAMMAR: Grammar = grammar!{
239        (?ibBw)
240
241        TOP -> r(r"\A") <time_expression> r(r"\z")
242
243        // non-terminal patterns
244        // these are roughly ordered by dependency
245
246        time_expression => <universal> | <particular>
247
248        particular => <one_time> | <two_times>
249
250        one_time => <moment_or_period>
251
252        two_times -> ("from")? <moment_or_period> <to> <moment_or_period> | <since_time>
253
254        since_time -> <since> <clusivity>? <moment_or_period>
255
256        clusivity -> ("the") <terminus> ("of")
257
258        terminus => <beginning> | <end>
259
260        to => <up_to> | <through>
261
262        moment_or_period => <moment> | <period>
263
264        period => <named_period> | <specific_period>
265
266        specific_period => <modified_period> | <month_and_year> | <year> | <relative_period>
267
268        modified_period -> <modifier>? <modifiable_period>
269
270        modifiable_period => [["week", "month", "year", "pay period", "payperiod", "pp", "weekend"]] | <a_month> | <a_day>
271
272        month_and_year -> <a_month> <year>
273
274        year => <short_year> | ("-")? <n_year>
275        year -> <suffix_year> <year_suffix>
276
277        year_suffix => <ce> | <bce>
278
279        relative_period -> <count> <displacement> <from_now_or_ago>
280
281        count => r(r"[1-9][0-9]*") | <a_count>
282
283        named_period => <a_day> | <a_month>
284
285        moment -> <adjustment>? <point_in_time>
286
287        adjustment -> <amount> <direction> // two minutes before
288
289        amount -> <count> <unit>
290
291        point_in_time -> <at_time_on>? <some_day> <at_time>? | <specific_time> | <time>
292
293        at_time_on -> ("at")? <time> ("on")?
294
295        some_day => <specific_day> | <relative_day>
296
297        specific_day => <adverb> | <date_with_year>
298
299        date_with_year => <n_date> | <a_date>
300
301        n_date -> <year>    r("[./-]") <n_month> r("[./-]") <n_day>
302        n_date -> <year>    r("[./-]") <n_day>   r("[./-]") <n_month>
303        n_date -> <n_month> r("[./-]") <n_day>   r("[./-]") <year>
304        n_date -> <n_day>   r("[./-]") <n_month> r("[./-]") <year>
305
306        a_date -> <day_prefix>? <a_month> <o_n_day> (",") <year>
307        a_date -> <day_prefix>? <n_day> <a_month> <year>
308        a_date -> <day_prefix>? ("the") <o_day> ("of") <a_month> <year>
309
310        day_prefix => <a_day> (",")?
311
312        relative_day => <a_day> | <a_day_in_month>
313
314        at_time -> ("at") <time>
315
316        specific_time => <first_time> | <last_time> | <precise_time>
317
318        precise_time -> <n_date> <hour_24>
319
320        time -> <hour_12> <am_pm>? | <hour_24> | <named_time>
321
322        hour_12 => <h12>
323        hour_12 => <h12> (":") <minute>
324        hour_12 => <h12> (":") <minute> (":") <second>
325
326        hour_24 => <h24>
327        hour_24 => <h24> (":") <minute>
328        hour_24 => <h24> (":") <minute> (":") <second>
329
330        a_day_in_month => <ordinal_day> | <day_and_month>
331
332        ordinal_day   -> <day_prefix>? ("the") <o_day>    // the first
333
334        o_day => <n_ordinal> | <a_ordinal> | <roman>
335
336        day_and_month -> <n_month> r("[./-]") <n_day>     // 5-6
337        day_and_month -> <a_month> ("the")? <o_n_day>     // June 5, June 5th, June fifth, June the fifth
338        day_and_month -> ("the") <o_day> ("of") <a_month> // the 5th of June, the fifth of June
339
340        o_n_day => <n_day> | <o_day>
341
342        // terminal patterns
343        // these are organized into single-line and multi-line patterns, with each group alphabetized
344
345        // various phrases all meaning from the first measurable moment to the last
346        a_count         => [["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"]]
347        adverb          => [["now", "today", "tomorrow", "yesterday"]]
348        am_pm           => (?-ib) [["am", "AM", "pm", "PM", "a.m.", "A.M.", "p.m.", "P.M."]]
349        bce             => (?-ib) [["bce", "b.c.e.", "bc", "b.c.", "BCE", "B.C.E.", "BC", "B.C."]]
350        beginning       => [["beginning", "start"]]
351        ce              => (?-ib) [["ce", "c.e.", "ad", "a.d.", "CE", "C.E.", "AD", "A.D."]]
352        direction       -> [["before", "after", "around", "before and after"]]
353        displacement    => [["week", "day", "hour", "minute", "second"]] ("s")?   // not handling variable-width periods like months or years
354        end             => ("end")
355        from_now_or_ago => [["from now", "ago"]]
356        h12             => (?-B) [(1..=12).into_iter().collect::<Vec<_>>()]
357        h24             => [(1..=24).into_iter().flat_map(|i| vec![format!("{}", i), format!("{:02}", i)]).collect::<Vec<_>>()]
358        minute          => (?-B) [ (0..60).into_iter().map(|i| format!("{:02}", i)).collect::<Vec<_>>() ]
359        modifier        => [["the", "this", "last", "next"]]
360        named_time      => [["noon", "midnight"]]
361        n_year          => r(r"\b(?:[1-9][0-9]{0,4}|0)\b")
362        roman           => [["nones", "ides", "kalends"]]
363        since           => [["since", "after"]]
364        unit            => [["week", "day", "hour", "minute", "second"]] ("s")?
365        universal       => [["always", "ever", "all time", "forever", "from beginning to end", "from the beginning to the end"]]
366        up_to           => [["to", "until", "up to", "till"]]
367        second          => (?-B) [ (0..60).into_iter().map(|i| format!("{:02}", i)).collect::<Vec<_>>() ]
368        suffix_year     => r(r"\b[1-9][0-9]{0,4}")
369        through         => [["up through", "through", "thru"]] | r("-+")
370
371        a_day => (?-i) [["M", "T", "W", "R", "F", "S", "U"]]
372        a_day => [
373                "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Tues Weds Thurs Tues. Weds. Thurs."
374                    .split(" ")
375                    .into_iter()
376                    .flat_map(|w| vec![
377                        w.to_string(),
378                        w[0..2].to_string(),
379                        w[0..3].to_string(),
380                        format!("{}.", w[0..2].to_string()),
381                        format!("{}.", w[0..3].to_string()),
382                    ])
383                    .collect::<Vec<_>>()
384            ]
385        a_month => [
386                "January February March April May June July August September October November December"
387                     .split(" ")
388                     .into_iter()
389                     .flat_map(|w| vec![w.to_string(), w[0..3].to_string()])
390                     .collect::<Vec<_>>()
391            ]
392        a_ordinal => [[
393                "first",
394                "second",
395                "third",
396                "fourth",
397                "fifth",
398                "sixth",
399                "seventh",
400                "eighth",
401                "ninth",
402                "tenth",
403                "eleventh",
404                "twelfth",
405                "thirteenth",
406                "fourteenth",
407                "fifteenth",
408                "sixteenth",
409                "seventeenth",
410                "eighteenth",
411                "nineteenth",
412                "twentieth",
413                "twenty-first",
414                "twenty-second",
415                "twenty-third",
416                "twenty-fourth",
417                "twenty-fifth",
418                "twenty-sixth",
419                "twenty-seventh",
420                "twenty-eighth",
421                "twenty-ninth",
422                "thirtieth",
423                "thirty-first"
424            ]]
425        first_time => [[
426                "the beginning",
427                "the beginning of time",
428                "the first moment",
429                "the start",
430                "the very start",
431                "the first instant",
432                "the dawn of time",
433                "the big bang",
434                "the birth of the universe",
435            ]]
436        last_time => [[
437                "the end",
438                "the end of time",
439                "the very end",
440                "the last moment",
441                "eternity",
442                "infinity",
443                "doomsday",
444                "the crack of doom",
445                "armageddon",
446                "ragnarok",
447                "the big crunch",
448                "the heat death of the universe",
449                "doom",
450                "death",
451                "perdition",
452                "the last hurrah",
453                "ever after",
454                "the last syllable of recorded time",
455            ]]
456        n_day => [
457                (1..=31)
458                    .into_iter()
459                    .flat_map(|i| vec![i.to_string(), format!("{:02}", i)])
460                    .collect::<Vec<_>>()
461            ]
462        n_month => [
463                (1..=12)
464                    .into_iter()
465                    .flat_map(|i| vec![format!("{:02}", i), format!("{}", i)])
466                    .collect::<Vec<_>>()
467            ]
468        n_ordinal => [[
469                "1st",
470                "2nd",
471                "3rd",
472                "4th",
473                "5th",
474                "6th",
475                "7th",
476                "8th",
477                "9th",
478                "10th",
479                "11th",
480                "12th",
481                "13th",
482                "14th",
483                "15th",
484                "16th",
485                "17th",
486                "18th",
487                "19th",
488                "20th",
489                "21st",
490                "22nd",
491                "23rd",
492                "24th",
493                "25th",
494                "26th",
495                "27th",
496                "28th",
497                "29th",
498                "30th",
499                "31st",
500            ]]
501        short_year => [
502                (0..=99)
503                    .into_iter()
504                    .flat_map(|i| vec![format!("'{:02}", i), format!("{:02}", i)])
505                    .collect::<Vec<_>>()
506            ]
507    };
508}
509// code generated via cargo run --bin serializer
510// this saves the cost of generating GRAMMAR
511lazy_static! {
512    #[doc(hidden)]
513    pub static ref MATCHER: Matcher = GRAMMAR.matcher().unwrap();
514}
515lazy_static! {
516    // making this public is useful for testing, but best to keep it hidden to
517    // limit complexity and commitment
518    #[doc(hidden)]
519    // this is a stripped-down version of GRAMMAR that just containst the most commonly used expressions
520    pub static ref SMALL_GRAMMAR: Grammar = grammar!{
521        (?ibBw)
522
523        TOP -> r(r"\A") <time_expression> r(r"\z")
524
525        // non-terminal patterns
526        // these are roughly ordered by dependency
527
528        time_expression => <particular>
529
530        particular => <one_time>
531
532        one_time => <moment_or_period>
533
534        moment_or_period => <moment> | <period>
535
536        period => <named_period> | <specific_period>
537
538        specific_period => <modified_period>
539
540        modified_period -> <modifier>? <modifiable_period>
541
542        modifiable_period => [["week", "month", "year", "pay period", "pp"]] | <a_month> | <a_day>
543
544        named_period => <a_day> | <a_month>
545
546        moment -> <point_in_time>
547
548        point_in_time -> <some_day>
549
550        some_day => <specific_day> | <relative_day>
551
552        specific_day => <adverb>
553
554        relative_day => <a_day>
555
556        // terminal patterns
557        // these are organized into single-line and multi-line patterns, with each group alphabetized
558
559        // various phrases all meaning from the first measurable moment to the last
560        adverb          => [["now", "today", "yesterday"]]
561        modifier        => [["the", "this", "last"]]
562
563        a_day => [
564                "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Tues Weds Thurs Tues. Weds. Thurs."
565                    .split(" ")
566                    .into_iter()
567                    .flat_map(|w| vec![
568                        w.to_string(),
569                        w[0..2].to_string(),
570                        w[0..3].to_string(),
571                        format!("{}.", w[0..2].to_string()),
572                        format!("{}.", w[0..3].to_string()),
573                    ])
574                    .collect::<Vec<_>>()
575            ]
576        a_month => [
577                "January February March April May June July August September October November December"
578                     .split(" ")
579                     .into_iter()
580                     .flat_map(|w| vec![w.to_string(), w[0..3].to_string()])
581                     .collect::<Vec<_>>()
582            ]
583    };
584}
585// code generated via cargo run --bin serializer
586// this saves the cost of generating GRAMMAR
587lazy_static! {
588    #[doc(hidden)]
589    pub static ref SMALL_MATCHER : Matcher = SMALL_GRAMMAR.matcher().unwrap();
590}
591
592/// Simply returns whether the given phrase is parsable as a time expression. This is slightly
593/// more efficient than `parse(expression, None).is_ok()` as no parse tree is generated.
594///
595/// # Examples
596///
597/// ```rust
598/// # extern crate two_timer;
599/// # use two_timer::{parsable};
600/// let copacetic = parsable("5/6/69");
601/// ```
602pub fn parsable(phrase: &str) -> bool {
603    if cfg!(feature = "small_grammar") {
604        SMALL_MATCHER.rx.is_match(phrase) || MATCHER.rx.is_match(phrase)
605    } else {
606        MATCHER.rx.is_match(phrase)
607    }
608}
609
610/// Converts a time expression into a pair or timestamps and a boolean indicating whether
611/// the expression was literally a range, such as "9 to 11", as opposed to "9 AM", say.
612///
613/// The second parameter is an optional `Config` object. In general you will not need to
614/// use this except in testing or in the interpretation of pay periods.
615///
616/// # Examples
617///
618/// ```rust
619/// # extern crate two_timer;
620/// # use two_timer::{parse, Config};
621/// let (reference_time, _, _) = parse("5/6/69", None).unwrap();
622/// ```
623pub fn parse(
624    phrase: &str,
625    config: Option<Config>,
626) -> Result<(NaiveDateTime, NaiveDateTime, bool), TimeError> {
627    let parse = if cfg!(feature = "small_grammar") {
628        SMALL_MATCHER
629            .parse(phrase)
630            .or_else(|| MATCHER.parse(phrase))
631    } else {
632        MATCHER.parse(phrase)
633    };
634    if parse.is_none() {
635        return Err(TimeError::Parse(format!(
636            "could not parse \"{}\" as a time expression",
637            phrase
638        )));
639    }
640    let parse = parse.unwrap();
641    if parse.has("universal") {
642        return Ok((first_moment(), last_moment(), false));
643    }
644    let parse = parse.name("particular").unwrap();
645    let config = config.unwrap_or(Config::new());
646    if let Some(moment) = parse.name("one_time") {
647        return match handle_one_time(moment, &config) {
648            Err(e) => Err(e),
649            Ok((d1, d2, b)) => {
650                let (d3, d4) = adjust(d1, d2, moment);
651                if d1 == d3 {
652                    Ok((d1, d2, b))
653                } else {
654                    Ok((d3, d4, b))
655                }
656            }
657        };
658    }
659    if let Some(two_times) = parse.name("two_times") {
660        let mut inclusive = two_times.has("beginning");
661        let exclusive = !inclusive && two_times.has("end"); // note this is *explicitly* exclusive
662        if !(inclusive || exclusive) && (two_times.has("time") || two_times.has("precise_time")) {
663            // treating "since noon" as including 12:00:00 and "since 2am" as including 14:00:00
664            inclusive = true;
665        }
666        if let Some(previous_time) = two_times.name("since_time") {
667            if specific(previous_time) {
668                return match specific_moment(previous_time, &config) {
669                    Ok((d1, d2)) => {
670                        let t = if inclusive { d1 } else { d2 };
671                        // if *implicitly* exclusive and we find things misordered, we become inclusive
672                        let t = if !(inclusive || exclusive) && t > config.now {
673                            d1
674                        } else {
675                            t
676                        };
677                        if t > config.now {
678                            Err(TimeError::Misordered(format!(
679                                "the inferred times, {} and {}, are misordered",
680                                t, config.now
681                            )))
682                        } else {
683                            Ok((t, config.now.clone(), false))
684                        }
685                    }
686                    Err(s) => Err(s),
687                };
688            }
689            return match relative_moment(previous_time, &config, &config.now, true) {
690                Ok((d1, d2)) => {
691                    let t = if inclusive { d1 } else { d2 };
692                    let t = if !(inclusive || exclusive) && t > config.now {
693                        d1
694                    } else {
695                        t
696                    };
697                    if t > config.now {
698                        Err(TimeError::Misordered(format!(
699                            "the inferred times, {} and {}, are misordered",
700                            t, config.now
701                        )))
702                    } else {
703                        Ok((t, config.now.clone(), false))
704                    }
705                }
706                Err(s) => Err(s),
707            };
708        }
709        let first = &two_times.children().unwrap()[0];
710        let last = &two_times.children().unwrap()[2];
711        let is_through = two_times.has("through");
712        if specific(first) {
713            if specific(last) {
714                return match specific_moment(first, &config) {
715                    Ok((d1, d2)) => {
716                        let (d1, _) = adjust(d1, d2, first);
717                        match specific_moment(last, &config) {
718                            Ok((d2, d3)) => {
719                                let (d2, d3) = adjust(d2, d3, last);
720                                let d2 = pick_terminus(d2, d3, is_through);
721                                if d1 <= d2 {
722                                    Ok((d1, d2, true))
723                                } else {
724                                    Err(TimeError::Misordered(format!(
725                                        "{} is after {}",
726                                        first.as_str(),
727                                        last.as_str()
728                                    )))
729                                }
730                            }
731                            Err(s) => Err(s),
732                        }
733                    }
734                    Err(s) => Err(s),
735                };
736            } else {
737                return match specific_moment(first, &config) {
738                    Ok((d1, d2)) => {
739                        let (d1, _) = adjust(d1, d2, first);
740                        match relative_moment(last, &config, &d1, false) {
741                            Ok((d2, d3)) => {
742                                let (d2, d3) = adjust(d2, d3, last);
743                                let d2 = pick_terminus(d2, d3, is_through);
744                                Ok((d1, d2, true))
745                            }
746                            Err(s) => Err(s),
747                        }
748                    }
749                    Err(s) => Err(s),
750                };
751            }
752        } else if specific(last) {
753            return match specific_moment(last, &config) {
754                Ok((d2, d3)) => {
755                    let (d2, d3) = adjust(d2, d3, last);
756                    let d2 = pick_terminus(d2, d3, is_through);
757                    match relative_moment(first, &config, &d2, true) {
758                        Ok((d1, d3)) => {
759                            let (d1, _) = adjust(d1, d3, first);
760                            Ok((d1, d2, true))
761                        }
762                        Err(s) => Err(s),
763                    }
764                }
765                Err(s) => Err(s),
766            };
767        } else {
768            // the first moment is assumed to be before now if default_to_past is true, otherwise it is after
769            return match relative_moment(first, &config, &config.now, config.default_to_past) {
770                Ok((d1, d2)) => {
771                    let (d1, _) = adjust(d1, d2, first);
772                    // the second moment is necessarily after the first moment
773                    match relative_moment(last, &config, &d1, false) {
774                        Ok((d2, d3)) => {
775                            let (d2, d3) = adjust(d2, d3, last);
776                            let d2 = pick_terminus(d2, d3, is_through);
777                            Ok((d1, d2, true))
778                        }
779                        Err(s) => Err(s),
780                    }
781                }
782                Err(s) => Err(s),
783            };
784        }
785    }
786    unreachable!();
787}
788
789/// A collection of parameters that can influence the interpretation
790/// of time expressions.
791#[derive(Debug, Clone)]
792pub struct Config {
793    now: NaiveDateTime,
794    monday_starts_week: bool,
795    period: Period,
796    pay_period_length: u32,
797    pay_period_start: Option<NaiveDate>,
798    default_to_past: bool,
799}
800
801impl Config {
802    /// Constructs an expression with the default parameters.
803    pub fn new() -> Config {
804        Config {
805            now: Local::now().naive_local(),
806            monday_starts_week: true,
807            period: Period::Minute,
808            pay_period_length: 7,
809            pay_period_start: None,
810            default_to_past: true,
811        }
812    }
813    /// Returns a copy of the configuration parameters with the "now" moment
814    /// set to the parameter supplied.
815    pub fn now(&self, n: NaiveDateTime) -> Config {
816        let mut c = self.clone();
817        c.now = n;
818        c
819    }
820    fn period(&self, period: Period) -> Config {
821        let mut c = self.clone();
822        c.period = period;
823        c
824    }
825    /// Returns a copy of the configuration parameters with whether
826    /// Monday is regarded as the first day of the week set to the parameter
827    /// supplied. By default Monday *is* regarded as the first day. If this
828    /// parameter is set to `false`, Sunday will be regarded as the first weekday.
829    pub fn monday_starts_week(&self, monday_starts_week: bool) -> Config {
830        let mut c = self.clone();
831        c.monday_starts_week = monday_starts_week;
832        c
833    }
834    /// Returns a copy of the configuration parameters with the pay period
835    /// length in days set to the parameter supplied. The default pay period
836    /// length is 7 days.
837    pub fn pay_period_length(&self, pay_period_length: u32) -> Config {
838        let mut c = self.clone();
839        c.pay_period_length = pay_period_length;
840        c
841    }
842    /// Returns a copy of the configuration parameters with the reference start
843    /// date for a pay period set to the parameter supplied. By default this date
844    /// is undefined. Unless it is defined, expressions containing the phrase "pay period"
845    /// or "pp" cannot be interpreted.
846    pub fn pay_period_start(&self, pay_period_start: Option<NaiveDate>) -> Config {
847        let mut c = self.clone();
848        c.pay_period_start = pay_period_start;
849        c
850    }
851    /// Returns a copy of the configuration parameters with the `default_to_past`
852    /// parameter set as specified. This allows the interpretation of relative time expressions
853    /// like "Friday" and "12:00". By default, these expressions are assumed to refer to the
854    /// most recent such interval in the *past*. By setting `default_to_past` to `false`
855    /// the rule changes so they are assumed to refer to the nearest such interval in the future.
856    pub fn default_to_past(&self, default_to_past: bool) -> Config {
857        let mut c = self.clone();
858        c.default_to_past = default_to_past;
859        c
860    }
861}
862
863/// A simple categorization of things that could go wrong.
864///
865/// Every error provides a descriptive string that can be displayed.
866#[derive(Debug, Clone)]
867pub enum TimeError {
868    /// The time expression cannot be parsed by the available grammar.
869    Parse(String),
870    /// The time expression consists of a time range and the end of the range is before
871    /// the beginning.
872    Misordered(String),
873    /// The time expression specifies an impossible date, such as the 31st of September.
874    ImpossibleDate(String),
875    /// The time expression specifies a weekday different from that required by the rest
876    /// of the expression, such as Wednesday, May 5, 1969, which was a Tuesday.
877    Weekday(String),
878    /// The time expression refers to a pay period, but the starting date of a reference
879    /// pay period has not been provided, so the pay period is undefined.
880    NoPayPeriod(String),
881}
882
883impl TimeError {
884    /// Extracts error message.
885    pub fn msg(&self) -> &str {
886        match self {
887            TimeError::Parse(s) => s.as_ref(),
888            TimeError::Misordered(s) => s.as_ref(),
889            TimeError::ImpossibleDate(s) => s.as_ref(),
890            TimeError::Weekday(s) => s.as_ref(),
891            TimeError::NoPayPeriod(s) => s.as_ref(),
892        }
893    }
894}
895
896impl std::error::Error for TimeError {}
897
898impl std::fmt::Display for TimeError {
899    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
900        match self {
901            TimeError::Parse(s) => write!(f, "Parse error: {}", s),
902            TimeError::Misordered(s) => write!(f, "Misordered error: {}", s),
903            TimeError::ImpossibleDate(s) => write!(f, "Impossible date: {}", s),
904            TimeError::Weekday(s) => write!(f, "Weekday error: {}", s),
905            TimeError::NoPayPeriod(s) => write!(f, "No Pay Period error: {}", s),
906        }
907    }
908}
909
910// for the end time, if the span is less than a day, use the first, otherwise use the second
911// e.g., Monday through Friday at 3 PM should end at 3 PM, but Monday through Friday should end at the end of Friday
912fn pick_terminus(d1: NaiveDateTime, d2: NaiveDateTime, through: bool) -> NaiveDateTime {
913    if through {
914        d2
915    } else {
916        d1
917    }
918}
919
920/// The moment regarded as the beginning of time.
921///
922/// # Examples
923///
924/// ```rust
925/// # extern crate two_timer;
926/// # use two_timer::first_moment;
927/// println!("{}", first_moment()); // -262144-01-01 00:00:00
928/// ```
929pub fn first_moment() -> NaiveDateTime {
930    NaiveDate::MIN.and_hms_milli_opt(0, 0, 0, 0).unwrap()
931}
932
933/// The moment regarded as the end of time.
934///
935/// # Examples
936///
937/// ```rust
938/// # extern crate two_timer;
939/// # use two_timer::last_moment;
940/// println!("{}", last_moment()); // +262143-12-31 23:59:59.999
941/// ```
942pub fn last_moment() -> NaiveDateTime {
943    NaiveDate::MAX.and_hms_milli_opt(23, 59, 59, 999).unwrap()
944}
945
946fn specific(m: &Match) -> bool {
947    m.has("specific_day") || m.has("specific_period") || m.has("specific_time")
948}
949
950fn n_date(date: &Match, config: &Config) -> Result<NaiveDate, TimeError> {
951    let year = year(date, config);
952    let month = n_month(date);
953    let day = n_day(date);
954    match NaiveDate::from_ymd_opt(year, month, day) {
955        None => Err(TimeError::ImpossibleDate(format!(
956            "cannot construct date with year {}, month {}, and day {}",
957            year, month, day
958        ))),
959        Some(d) => Ok(d),
960    }
961}
962
963fn handle_specific_day(
964    m: &Match,
965    config: &Config,
966) -> Result<(NaiveDateTime, NaiveDateTime), TimeError> {
967    let now = config.now.clone();
968    let mut times = m.all_names("time");
969    if times.len() > 1 {
970        return Err(TimeError::Parse(format!(
971            "more than one daytime specified in {}",
972            m.as_str()
973        )));
974    }
975    let time = times.pop();
976    if let Some(adverb) = m.name("adverb") {
977        return match adverb.as_str().chars().nth(0).expect("empty string") {
978            // now
979            'n' | 'N' => Ok(moment_and_time(config, time)),
980            't' | 'T' => match adverb.as_str().chars().nth(2).expect("impossible string") {
981                // today
982                'd' | 'D' => Ok(moment_and_time(&config.period(Period::Day), time)),
983                // tomorrow
984                'm' | 'M' => Ok(moment_and_time(
985                    &Config::new()
986                        .now(now + Duration::days(1))
987                        .period(Period::Day),
988                    time,
989                )),
990                _ => unreachable!(),
991            },
992            // yesterday
993            'y' | 'Y' => Ok(moment_and_time(
994                &Config::new()
995                    .now(now - Duration::days(1))
996                    .period(Period::Day),
997                time,
998            )),
999            _ => unreachable!(),
1000        };
1001    }
1002    if let Some(date) = m.name("date_with_year") {
1003        if let Some(date) = date.name("n_date") {
1004            return match n_date(date, config) {
1005                Err(s) => Err(s),
1006                Ok(d1) => {
1007                    let d1 = d1.and_hms_opt(0, 0, 0).unwrap();
1008                    Ok(moment_and_time(
1009                        &Config::new().now(d1).period(Period::Day),
1010                        time,
1011                    ))
1012                }
1013            };
1014        }
1015        if let Some(date) = date.name("a_date") {
1016            let year = year(date, config);
1017            let month = a_month(date);
1018            let day = if date.has("n_day") {
1019                n_day(date)
1020            } else {
1021                o_day(date, month)
1022            };
1023            let d_opt = NaiveDate::from_ymd_opt(year, month, day);
1024            return match d_opt {
1025                None => Err(TimeError::ImpossibleDate(format!(
1026                    "cannot construct date with year {}, month {}, and day {}",
1027                    year, month, day
1028                ))),
1029                Some(d1) => {
1030                    if let Some(wd) = date.name("a_day") {
1031                        let wd = weekday(wd.as_str());
1032                        if wd == d1.weekday() {
1033                            let d1 = d1.and_hms_opt(0, 0, 0).unwrap();
1034                            Ok(moment_and_time(
1035                                &Config::new().now(d1).period(Period::Day),
1036                                time,
1037                            ))
1038                        } else {
1039                            Err(TimeError::Weekday(format!(
1040                                "the weekday of year {}, month {}, day {} is not {}",
1041                                year,
1042                                month,
1043                                day,
1044                                date.name("a_day").unwrap().as_str()
1045                            )))
1046                        }
1047                    } else {
1048                        let d1 = d1.and_hms_opt(0, 0, 0).unwrap();
1049                        Ok(moment_and_time(
1050                            &Config::new().now(d1).period(Period::Day),
1051                            time,
1052                        ))
1053                    }
1054                }
1055            };
1056        }
1057        unreachable!();
1058    }
1059    unimplemented!();
1060}
1061
1062fn handle_specific_period(
1063    moment: &Match,
1064    config: &Config,
1065) -> Result<(NaiveDateTime, NaiveDateTime), TimeError> {
1066    if let Some(moment) = moment.name("relative_period") {
1067        let count = count(moment.name("count").unwrap()) as i64;
1068        let (displacement, period) = match moment
1069            .name("displacement")
1070            .unwrap()
1071            .as_str()
1072            .chars()
1073            .nth(0)
1074            .unwrap()
1075        {
1076            'w' | 'W' => (Duration::weeks(count), Period::Week),
1077            'd' | 'D' => (Duration::days(count), Period::Day),
1078            'h' | 'H' => (Duration::hours(count), Period::Hour),
1079            'm' | 'M' => (Duration::minutes(count), Period::Minute),
1080            's' | 'S' => (Duration::seconds(count), Period::Second),
1081            _ => unreachable!(),
1082        };
1083        let d = match moment
1084            .name("from_now_or_ago")
1085            .unwrap()
1086            .as_str()
1087            .chars()
1088            .nth(0)
1089            .unwrap()
1090        {
1091            'a' | 'A' => config.now - displacement,
1092            'f' | 'F' => config.now + displacement,
1093            _ => unreachable!(),
1094        };
1095        let span = match period {
1096            Period::Week => (d, d + Duration::weeks(1)),
1097            _ => moment_to_period(d, &period, config),
1098        };
1099        return Ok(span);
1100    }
1101    if let Some(moment) = moment.name("month_and_year") {
1102        let y = year(moment, &config);
1103        let m = a_month(moment);
1104        return match NaiveDate::from_ymd_opt(y, m, 1) {
1105            None => unreachable!(),
1106            Some(d1) => {
1107                let d1 = d1.and_hms_opt(0, 0, 0).unwrap();
1108                Ok(moment_and_time(
1109                    &Config::new().now(d1).period(Period::Month),
1110                    None,
1111                ))
1112            }
1113        };
1114    }
1115    if let Some(moment) = moment.name("modified_period") {
1116        let modifier = PeriodModifier::from_match(moment.name("modifier"));
1117        if let Some(month) = moment.name("a_month") {
1118            let d = config.now.with_month(a_month(month)).unwrap();
1119            let (d, _) = moment_to_period(d, &Period::Month, config);
1120            let d = match modifier {
1121                PeriodModifier::Next => d.with_year(d.year() + 1).unwrap(),
1122                PeriodModifier::Last => d.with_year(d.year() - 1).unwrap(),
1123                PeriodModifier::This => d,
1124            };
1125            return Ok(moment_to_period(d, &Period::Month, config));
1126        }
1127        if let Some(wd) = moment.name("a_day") {
1128            let wd = weekday(wd.as_str());
1129            let offset = config.now.weekday().num_days_from_monday() as i64
1130                - wd.num_days_from_monday() as i64;
1131            let d = config.now.date() - Duration::days(offset);
1132            let d = match modifier {
1133                PeriodModifier::Next => d + Duration::days(7),
1134                PeriodModifier::Last => d - Duration::days(7),
1135                PeriodModifier::This => d,
1136            };
1137            return Ok(moment_to_period(
1138                d.and_hms_opt(0, 0, 0).unwrap(),
1139                &Period::Day,
1140                config,
1141            ));
1142        }
1143        return match ModifiablePeriod::from_match(moment.name("modifiable_period").unwrap()) {
1144            ModifiablePeriod::Week => {
1145                let (d, _) = moment_to_period(config.now, &Period::Week, config);
1146                let d = match modifier {
1147                    PeriodModifier::Next => d + Duration::days(7),
1148                    PeriodModifier::Last => d - Duration::days(7),
1149                    PeriodModifier::This => d,
1150                };
1151                Ok(moment_to_period(d, &Period::Week, config))
1152            }
1153            ModifiablePeriod::Weekend => {
1154                let (_, d2) =
1155                    moment_to_period(config.now, &Period::Week, &config.monday_starts_week(true));
1156                let d2 = match modifier {
1157                    PeriodModifier::Next => d2 + Duration::days(7),
1158                    PeriodModifier::Last => d2 - Duration::days(7),
1159                    PeriodModifier::This => d2,
1160                };
1161                let d1 = d2 - Duration::days(2);
1162                Ok((d1, d2))
1163            }
1164            ModifiablePeriod::Month => {
1165                let (d, _) = moment_to_period(config.now, &Period::Month, config);
1166                let d = match modifier {
1167                    PeriodModifier::Next => {
1168                        if d.month() == 12 {
1169                            d.with_year(d.year() + 1).unwrap().with_month(1).unwrap()
1170                        } else {
1171                            d.with_month(d.month() + 1).unwrap()
1172                        }
1173                    }
1174                    PeriodModifier::Last => {
1175                        if d.month() == 1 {
1176                            d.with_year(d.year() - 1).unwrap().with_month(12).unwrap()
1177                        } else {
1178                            d.with_month(d.month() - 1).unwrap()
1179                        }
1180                    }
1181                    PeriodModifier::This => d,
1182                };
1183                Ok(moment_to_period(d, &Period::Month, config))
1184            }
1185            ModifiablePeriod::Year => {
1186                let (d, _) = moment_to_period(config.now, &Period::Year, config);
1187                let d = match modifier {
1188                    PeriodModifier::Next => d.with_year(d.year() + 1).unwrap(),
1189                    PeriodModifier::Last => d.with_year(d.year() - 1).unwrap(),
1190                    PeriodModifier::This => d,
1191                };
1192                Ok(moment_to_period(d, &Period::Year, config))
1193            }
1194            ModifiablePeriod::PayPeriod => {
1195                if config.pay_period_start.is_some() {
1196                    let (d, _) = moment_to_period(config.now, &Period::PayPeriod, config);
1197                    let d = match modifier {
1198                        PeriodModifier::Next => d + Duration::days(config.pay_period_length as i64),
1199                        PeriodModifier::Last => d - Duration::days(config.pay_period_length as i64),
1200                        PeriodModifier::This => d,
1201                    };
1202                    Ok(moment_to_period(d, &Period::PayPeriod, config))
1203                } else {
1204                    Err(TimeError::NoPayPeriod(String::from(
1205                        "no pay period start date provided",
1206                    )))
1207                }
1208            }
1209        };
1210    }
1211    if let Some(moment) = moment.name("year") {
1212        let year = year(moment, config);
1213        return Ok(moment_to_period(
1214            first_moment_of_day(year, 1, 1),
1215            &Period::Year,
1216            config,
1217        ));
1218    }
1219    unreachable!()
1220}
1221
1222enum ModifiablePeriod {
1223    Week,
1224    Month,
1225    Year,
1226    PayPeriod,
1227    Weekend,
1228}
1229
1230impl ModifiablePeriod {
1231    fn from_match(m: &Match) -> ModifiablePeriod {
1232        match m.as_str().chars().nth(0).expect("unreachable") {
1233            'w' | 'W' => {
1234                if m.as_str().len() == 4 {
1235                    ModifiablePeriod::Week
1236                } else {
1237                    ModifiablePeriod::Weekend
1238                }
1239            }
1240            'm' | 'M' => ModifiablePeriod::Month,
1241            'y' | 'Y' => ModifiablePeriod::Year,
1242            'p' | 'P' => ModifiablePeriod::PayPeriod,
1243            _ => unreachable!(),
1244        }
1245    }
1246}
1247
1248enum PeriodModifier {
1249    This,
1250    Next,
1251    Last,
1252}
1253
1254impl PeriodModifier {
1255    fn from_match(m: Option<&Match>) -> PeriodModifier {
1256        if let Some(m) = m {
1257            match m.as_str().chars().nth(0).expect("unreachable") {
1258                't' | 'T' => PeriodModifier::This,
1259                'l' | 'L' => PeriodModifier::Last,
1260                'n' | 'N' => PeriodModifier::Next,
1261                _ => unreachable!(),
1262            }
1263        } else {
1264            PeriodModifier::This
1265        }
1266    }
1267}
1268
1269fn handle_specific_time(
1270    moment: &Match,
1271    config: &Config,
1272) -> Result<(NaiveDateTime, NaiveDateTime), TimeError> {
1273    if let Some(moment) = moment.name("precise_time") {
1274        return match n_date(moment, config) {
1275            Err(s) => Err(s),
1276            Ok(d) => {
1277                let (hour, minute, second, _) = time(moment);
1278                let m = d.and_hms_opt(hour, minute, second).unwrap();
1279                Ok(moment_to_period(m, &Period::Second, config))
1280            }
1281        };
1282    }
1283    return if moment.has("first_time") {
1284        Ok(moment_to_period(first_moment(), &config.period, config))
1285    } else {
1286        Ok((last_moment(), last_moment()))
1287    };
1288}
1289
1290fn handle_one_time(
1291    moment: &Match,
1292    config: &Config,
1293) -> Result<(NaiveDateTime, NaiveDateTime, bool), TimeError> {
1294    let r = if moment.has("specific_day") {
1295        handle_specific_day(moment, config)
1296    } else if let Some(moment) = moment.name("specific_period") {
1297        handle_specific_period(moment, config)
1298    } else if let Some(moment) = moment.name("specific_time") {
1299        handle_specific_time(moment, config)
1300    } else {
1301        relative_moment(moment, config, &config.now, config.default_to_past)
1302    };
1303    match r {
1304        Ok((d1, d2)) => Ok((d1, d2, false)),
1305        Err(e) => Err(e),
1306    }
1307}
1308
1309// add time to a date
1310fn moment_and_time(config: &Config, daytime: Option<&Match>) -> (NaiveDateTime, NaiveDateTime) {
1311    if let Some(daytime) = daytime {
1312        let (hour, minute, second, is_midnight) = time(daytime);
1313        let mut m = config
1314            .now
1315            .with_hour(hour)
1316            .unwrap()
1317            .with_minute(minute)
1318            .unwrap()
1319            .with_second(second)
1320            .unwrap();
1321        if is_midnight {
1322            m = m + Duration::days(1); // midnight is second 0 *of the next day*
1323        }
1324        moment_to_period(m, &Period::Second, config)
1325    } else {
1326        moment_to_period(config.now, &config.period, config)
1327    }
1328}
1329
1330fn relative_moment(
1331    m: &Match,
1332    config: &Config,
1333    other_time: &NaiveDateTime,
1334    before: bool, // whether the time found should be before or after the reference time
1335) -> Result<(NaiveDateTime, NaiveDateTime), TimeError> {
1336    if let Some(a_month_and_a_day) = m.name("a_day_in_month") {
1337        return match month_and_a_day(a_month_and_a_day, config, other_time, before) {
1338            Ok(d) => Ok(moment_and_time(
1339                &config
1340                    .now(d.and_hms_opt(0, 0, 0).unwrap())
1341                    .period(Period::Day),
1342                m.name("time"),
1343            )),
1344            Err(e) => Err(e),
1345        };
1346    }
1347    if let Some(day) = m.name("a_day") {
1348        let wd = weekday(day.as_str());
1349        let mut delta =
1350            other_time.weekday().num_days_from_sunday() as i64 - wd.num_days_from_sunday() as i64;
1351        if delta <= 0 {
1352            delta += 7;
1353        }
1354        let mut d = other_time.date() - Duration::days(delta);
1355        if !before {
1356            d = d + Duration::days(7);
1357        }
1358        return Ok(moment_and_time(
1359            &config
1360                .now(d.and_hms_opt(0, 0, 0).unwrap())
1361                .period(Period::Day),
1362            m.name("time"),
1363        ));
1364    }
1365    if let Some(t) = m.name("time") {
1366        let (hour, minute, second, is_midnight) = time(t);
1367        let mut t = other_time
1368            .with_hour(hour)
1369            .unwrap()
1370            .with_minute(minute)
1371            .unwrap()
1372            .with_second(second)
1373            .unwrap();
1374        if is_midnight {
1375            t = t + Duration::days(1); // midnight is second 0 *of the next day*
1376        }
1377        if before && t > *other_time {
1378            t = t - Duration::days(1);
1379        } else if !before && t < *other_time {
1380            t = t + Duration::days(1);
1381        }
1382        return Ok(moment_to_period(t, &Period::Second, config));
1383    }
1384    if let Some(month) = m.name("a_month") {
1385        let month = a_month(month);
1386        let year = if before {
1387            if month > other_time.month() {
1388                other_time.year() - 1
1389            } else {
1390                other_time.year()
1391            }
1392        } else {
1393            if month < other_time.month() {
1394                other_time.year() + 1
1395            } else {
1396                other_time.year()
1397            }
1398        };
1399        let d = first_moment_of_day(year, month, 1);
1400        let (d1, d2) = moment_to_period(d, &Period::Month, config);
1401        if before && d1 >= *other_time {
1402            return Ok(moment_to_period(
1403                d1.with_year(d1.year() - 1).unwrap(),
1404                &Period::Month,
1405                config,
1406            ));
1407        } else if !before && d2 <= *other_time {
1408            return Ok(moment_to_period(
1409                d1.with_year(d1.year() + 1).unwrap(),
1410                &Period::Month,
1411                config,
1412            ));
1413        }
1414        return Ok((d1, d2));
1415    }
1416    unreachable!()
1417}
1418
1419// for things like "the fifth", "March fifth", "5-6"
1420fn month_and_a_day(
1421    m: &Match,
1422    config: &Config,
1423    other_time: &NaiveDateTime,
1424    before: bool,
1425) -> Result<NaiveDate, TimeError> {
1426    if m.has("ordinal_day") {
1427        let mut year = config.now.year();
1428        let mut month = other_time.month();
1429        let day = o_day(m, month);
1430        let wd = if let Some(a_day) = m.name("a_day") {
1431            Some(weekday(a_day.as_str()))
1432        } else {
1433            None
1434        };
1435        // search backwards through the calendar for a possible day
1436        for _ in 0..4 * 7 * 12 {
1437            if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
1438                if wd.is_none() || d.weekday() == wd.unwrap() {
1439                    return Ok(d);
1440                }
1441            }
1442            if month == 1 {
1443                month = 12;
1444                year -= 1;
1445            } else {
1446                month -= 1;
1447            }
1448        }
1449        return Err(TimeError::ImpossibleDate(format!(
1450            "there is no day {} in the year {}",
1451            m.as_str(),
1452            config.now.year()
1453        )));
1454    }
1455    let (month, day) = if let Some(month) = m.name("n_month") {
1456        let month = n_month(month);
1457        let day = m.name("n_day").unwrap();
1458        (month, n_day(day))
1459    } else {
1460        let month = a_month(m);
1461        let day = if let Some(day) = m.name("n_day") {
1462            n_day(day)
1463        } else {
1464            o_day(m, month)
1465        };
1466        (month, day)
1467    };
1468    let year = if before {
1469        config.now.year()
1470    } else {
1471        if month < other_time.month() {
1472            other_time.year() + 1
1473        } else {
1474            other_time.year()
1475        }
1476    };
1477    match NaiveDate::from_ymd_opt(year, month, day) {
1478        Some(d) => Ok(d),
1479        None => Err(TimeError::ImpossibleDate(format!(
1480            "could not construct date from {} with year {}, month {}, and day {}",
1481            m.as_str(),
1482            year,
1483            month,
1484            day
1485        ))),
1486    }
1487}
1488
1489fn specific_moment(
1490    m: &Match,
1491    config: &Config,
1492) -> Result<(NaiveDateTime, NaiveDateTime), TimeError> {
1493    if m.has("specific_day") {
1494        return handle_specific_day(m, config);
1495    }
1496    if let Some(m) = m.name("specific_period") {
1497        return handle_specific_period(m, config);
1498    }
1499    if let Some(m) = m.name("specific_time") {
1500        return handle_specific_time(m, config);
1501    }
1502    unreachable!()
1503}
1504
1505fn a_month(m: &Match) -> u32 {
1506    match m.name("a_month").unwrap().as_str()[0..3]
1507        .to_lowercase()
1508        .as_ref()
1509    {
1510        "jan" => 1,
1511        "feb" => 2,
1512        "mar" => 3,
1513        "apr" => 4,
1514        "may" => 5,
1515        "jun" => 6,
1516        "jul" => 7,
1517        "aug" => 8,
1518        "sep" => 9,
1519        "oct" => 10,
1520        "nov" => 11,
1521        "dec" => 12,
1522        _ => unreachable!(),
1523    }
1524}
1525
1526// extract hour, minute, and second from time match
1527// last parameter is basically whether the value returned is for "midnight", which requires special handling
1528fn time(m: &Match) -> (u32, u32, u32, bool) {
1529    if let Some(m) = m.name("named_time") {
1530        return match m.as_str().chars().nth(0).unwrap() {
1531            'n' | 'N' => (12, 0, 0, false),
1532            _ => (0, 0, 0, true),
1533        };
1534    }
1535    let hour = if let Some(hour_24) = m.name("hour_24") {
1536        let hour = s_to_n(hour_24.name("h24").unwrap().as_str());
1537        if hour == 24 {
1538            0
1539        } else {
1540            hour
1541        }
1542    } else if let Some(hour_12) = m.name("hour_12") {
1543        let mut hour = s_to_n(hour_12.name("h12").unwrap().as_str());
1544        hour = if let Some(am_pm) = m.name("am_pm") {
1545            match am_pm.as_str().chars().nth(0).expect("empty string") {
1546                'a' | 'A' => hour,
1547                _ => hour + 12,
1548            }
1549        } else {
1550            hour
1551        };
1552        if hour == 24 {
1553            0
1554        } else {
1555            hour
1556        }
1557    } else {
1558        unreachable!()
1559    };
1560    if let Some(minute) = m.name("minute") {
1561        let minute = s_to_n(minute.as_str());
1562        if let Some(second) = m.name("second") {
1563            let second = s_to_n(second.as_str());
1564            (hour, minute, second, false)
1565        } else {
1566            (hour, minute, 0, false)
1567        }
1568    } else {
1569        (hour, 0, 0, false)
1570    }
1571}
1572
1573fn n_month(m: &Match) -> u32 {
1574    lazy_static! {
1575        static ref MONTH: Regex = Regex::new(r"\A0?(\d{1,2})\z").unwrap();
1576    }
1577    let cap = MONTH.captures(m.name("n_month").unwrap().as_str()).unwrap();
1578    cap[1].parse::<u32>().unwrap()
1579}
1580
1581fn year(m: &Match, config: &Config) -> i32 {
1582    let year = m.name("year").unwrap();
1583    if let Some(sy) = year.name("short_year") {
1584        let y = s_to_n(sy.as_str()) as i32;
1585        let this_year = config.now.year() % 100;
1586        if config.default_to_past {
1587            if this_year < y {
1588                // previous century
1589                config.now.year() - this_year - 100 + y
1590            } else {
1591                // this century
1592                config.now.year() - this_year + y
1593            }
1594        } else {
1595            if this_year > y {
1596                // next century
1597                config.now.year() - this_year + 100 + y
1598            } else {
1599                // this century
1600                config.now.year() - this_year + y
1601            }
1602        }
1603    } else if let Some(suffix) = year.name("year_suffix") {
1604        let y = s_to_n(year.name("suffix_year").unwrap().as_str()) as i32;
1605        if suffix.has("bce") {
1606            1 - y // there is no year 0
1607        } else {
1608            y
1609        }
1610    } else {
1611        let y = s_to_n(year.name("n_year").unwrap().as_str()) as i32;
1612        if year.as_str().chars().nth(0).expect("unreachable") == '-' {
1613            -y
1614        } else {
1615            y
1616        }
1617    }
1618}
1619
1620fn s_to_n(s: &str) -> u32 {
1621    lazy_static! {
1622        static ref S_TO_N: Regex = Regex::new(r"\A[\D0]*(\d+)\z").unwrap();
1623    }
1624    S_TO_N.captures(s).unwrap()[1].parse::<u32>().unwrap()
1625}
1626
1627fn n_day(m: &Match) -> u32 {
1628    m.name("n_day").unwrap().as_str().parse::<u32>().unwrap()
1629}
1630
1631fn o_day(m: &Match, month: u32) -> u32 {
1632    let m = m.name("o_day").unwrap();
1633    let s = m.as_str();
1634    if m.has("a_ordinal") {
1635        ordinal(s)
1636    } else if m.has("n_ordinal") {
1637        s[0..s.len() - 2].parse::<u32>().unwrap()
1638    } else {
1639        // roman
1640        match s.chars().nth(0).expect("empty string") {
1641            'n' | 'N' => {
1642                // nones
1643                match month {
1644                    3 | 5 | 7 | 10 => 7, // March, May, July, October
1645                    _ => 5,
1646                }
1647            }
1648            'i' | 'I' => {
1649                // ides
1650                match month {
1651                    3 | 5 | 7 | 10 => 15, // March, May, July, October
1652                    _ => 13,
1653                }
1654            }
1655            _ => 1, // kalends
1656        }
1657    }
1658}
1659
1660// converts the ordinals up to thirty-first
1661fn ordinal(s: &str) -> u32 {
1662    match s.chars().nth(0).expect("empty string") {
1663        'f' | 'F' => {
1664            match s.chars().nth(1).expect("too short") {
1665                'i' | 'I' => {
1666                    match s.chars().nth(2).expect("too short") {
1667                        'r' | 'R' => 1, // first
1668                        _ => {
1669                            if s.len() == 5 {
1670                                5 // fifth
1671                            } else {
1672                                15 // fifteenth
1673                            }
1674                        }
1675                    }
1676                }
1677                _ => {
1678                    if s.len() == 6 {
1679                        4 // fourth
1680                    } else {
1681                        14 // fourteenth
1682                    }
1683                }
1684            }
1685        }
1686        's' | 'S' => {
1687            match s.chars().nth(1).expect("too short") {
1688                'e' | 'E' => {
1689                    match s.len() {
1690                        6 => 2,  // second
1691                        7 => 7,  // seventh
1692                        _ => 17, // seventeenth
1693                    }
1694                }
1695                _ => {
1696                    if s.len() == 5 {
1697                        6 // sixth
1698                    } else {
1699                        16 // sixteenth
1700                    }
1701                }
1702            }
1703        }
1704        't' | 'T' => {
1705            match s.chars().nth(1).expect("too short") {
1706                'h' | 'H' => {
1707                    match s.chars().nth(4).expect("too short") {
1708                        'd' | 'D' => 3, //third
1709                        _ => {
1710                            match s.chars().nth(5).expect("too short") {
1711                                'e' | 'E' => 13, // thirteenth
1712                                'i' | 'I' => 30, // thirtieth
1713                                _ => 31,         // thirty-first
1714                            }
1715                        }
1716                    }
1717                }
1718                'e' | 'E' => 10, // tenth
1719                _ => {
1720                    match s.chars().nth(3).expect("too short") {
1721                        'l' | 'L' => 12, // twelfth
1722                        _ => {
1723                            if s.len() == 9 {
1724                                20 // twentiety
1725                            } else {
1726                                20 + ordinal(&s[7..s.len()]) // twenty-first...
1727                            }
1728                        }
1729                    }
1730                }
1731            }
1732        }
1733        'e' | 'E' => {
1734            match s.chars().nth(1).expect("too short") {
1735                'i' | 'I' => {
1736                    if s.len() == 6 {
1737                        8 // eight
1738                    } else {
1739                        18 // eighteen
1740                    }
1741                }
1742                _ => 11, // eleventh
1743            }
1744        }
1745        _ => {
1746            if s.len() == 5 {
1747                9 // ninth
1748            } else {
1749                19 // nineteenth
1750            }
1751        }
1752    }
1753}
1754
1755/// expand a moment to the period containing it
1756fn moment_to_period(
1757    now: NaiveDateTime,
1758    period: &Period,
1759    config: &Config,
1760) -> (NaiveDateTime, NaiveDateTime) {
1761    match period {
1762        Period::Year => {
1763            let d1 = first_moment_of_day(now.year(), 1, 1);
1764            let d2 = first_moment_of_day(now.year() + 1, 1, 1);
1765            (d1, d2)
1766        }
1767        Period::Month => {
1768            let d1 = first_moment_of_day(now.year(), now.month(), 1);
1769            let d2 = if now.month() == 12 {
1770                first_moment_of_day(now.year() + 1, 1, 1)
1771            } else {
1772                first_moment_of_day(now.year(), now.month() + 1, 1)
1773            };
1774            (d1, d2)
1775        }
1776        Period::Week => {
1777            let offset = if config.monday_starts_week {
1778                now.weekday().num_days_from_monday()
1779            } else {
1780                now.weekday().num_days_from_sunday()
1781            };
1782            let d1 = first_moment_of_day(now.year(), now.month(), now.day())
1783                - Duration::days(offset as i64);
1784            (d1, d1 + Duration::days(7))
1785        }
1786        Period::Day => {
1787            let d1 = first_moment_of_day(now.year(), now.month(), now.day());
1788            (d1, d1 + Duration::days(1))
1789        }
1790        Period::Hour => {
1791            let d1 = precise_moment(now.year(), now.month(), now.day(), now.hour(), 0, 0);
1792            (d1, d1 + Duration::hours(1))
1793        }
1794        Period::Minute => {
1795            let d1 = precise_moment(
1796                now.year(),
1797                now.month(),
1798                now.day(),
1799                now.hour(),
1800                now.minute(),
1801                0,
1802            );
1803            (d1, d1 + Duration::minutes(1))
1804        }
1805        Period::Second => {
1806            let d1 = precise_moment(
1807                now.year(),
1808                now.month(),
1809                now.day(),
1810                now.hour(),
1811                now.minute(),
1812                now.second(),
1813            );
1814            (d1, d1 + Duration::seconds(1))
1815        }
1816        Period::PayPeriod => {
1817            if let Some(pps) = config.pay_period_start {
1818                // find the current pay period start
1819                let offset = now.num_days_from_ce() - pps.num_days_from_ce();
1820                let ppl = config.pay_period_length as i32;
1821                let mut offset = (offset % ppl) as i64;
1822                if offset < 0 {
1823                    offset += config.pay_period_length as i64;
1824                }
1825                let d1 = (now.date() - Duration::days(offset))
1826                    .and_hms_opt(0, 0, 0)
1827                    .unwrap();
1828                (d1, d1 + Duration::days(config.pay_period_length as i64))
1829            } else {
1830                unreachable!()
1831            }
1832        }
1833    }
1834}
1835
1836#[derive(Debug, Clone)]
1837enum Period {
1838    Year,
1839    Month,
1840    Week,
1841    Day,
1842    Hour,
1843    Minute,
1844    Second,
1845    PayPeriod,
1846}
1847
1848fn weekday(s: &str) -> Weekday {
1849    match s.chars().nth(0).expect("empty string") {
1850        'm' | 'M' => Weekday::Mon,
1851        't' | 'T' => {
1852            if s.len() == 1 {
1853                Weekday::Tue
1854            } else {
1855                match s.chars().nth(1).unwrap() {
1856                    'u' | 'U' => Weekday::Tue,
1857                    'h' | 'H' => Weekday::Thu,
1858                    _ => unreachable!(),
1859                }
1860            }
1861        }
1862        'w' | 'W' => Weekday::Wed,
1863        'H' => Weekday::Thu,
1864        'F' | 'f' => Weekday::Fri,
1865        'S' | 's' => {
1866            if s.len() == 1 {
1867                Weekday::Sat
1868            } else {
1869                match s.chars().nth(1).unwrap() {
1870                    'a' | 'A' => Weekday::Sat,
1871                    'u' | 'U' => Weekday::Sun,
1872                    _ => unreachable!(),
1873                }
1874            }
1875        }
1876        'U' => Weekday::Sun,
1877        _ => unreachable!(),
1878    }
1879}
1880
1881// adjust a period relative to another period -- e.g., "one week before June" or "five minutes around 12:00 PM"
1882fn adjust(d1: NaiveDateTime, d2: NaiveDateTime, m: &Match) -> (NaiveDateTime, NaiveDateTime) {
1883    if let Some(adjustment) = m.name("adjustment") {
1884        let count = count(adjustment.name("count").unwrap()) as i64;
1885        let unit = match adjustment
1886            .name("unit")
1887            .unwrap()
1888            .as_str()
1889            .chars()
1890            .nth(0)
1891            .unwrap()
1892        {
1893            'w' | 'W' => Duration::weeks(count),
1894            'd' | 'D' => Duration::days(count),
1895            'h' | 'H' => Duration::hours(count),
1896            'm' | 'M' => Duration::minutes(count),
1897            _ => Duration::seconds(count),
1898        };
1899        let direction = adjustment.name("direction").unwrap().as_str();
1900        match direction.chars().nth(0).unwrap() {
1901            'b' | 'B' => {
1902                if direction.len() == 6 {
1903                    // before
1904                    let d = d1 - unit;
1905                    (d, d)
1906                } else {
1907                    // before and after
1908                    (d1 - unit, d1 + unit)
1909                }
1910            }
1911            _ => match direction.chars().nth(1).unwrap() {
1912                'f' | 'F' => {
1913                    let d = d2 + unit;
1914                    (d, d)
1915                }
1916                _ => {
1917                    let d1 = d1 - Duration::milliseconds(unit.num_milliseconds() / 2);
1918                    let d2 = d1 + unit;
1919                    (d1, d2)
1920                }
1921            },
1922        }
1923    } else {
1924        (d1, d2)
1925    }
1926}
1927
1928// for converting a few cardinal numbers and integer expressions
1929fn count(m: &Match) -> u32 {
1930    let s = m.as_str();
1931    if m.has("a_count") {
1932        // cardinal numbers
1933        match s.chars().nth(0).expect("impossibly short") {
1934            'o' | 'O' => 1,
1935            't' | 'T' => match s.chars().nth(1).expect("impossibly short") {
1936                'w' | 'W' => 2,
1937                'h' | 'H' => 3,
1938                _ => 10,
1939            },
1940            'f' | 'F' => match s.chars().nth(1).expect("impossibly short") {
1941                'o' | 'O' => 4,
1942                _ => 5,
1943            },
1944            's' | 'S' => match s.chars().nth(1).expect("impossibly short") {
1945                'i' | 'I' => 6,
1946                _ => 7,
1947            },
1948            'e' | 'E' => 8,
1949            _ => 9,
1950        }
1951    } else {
1952        s.parse::<u32>().unwrap()
1953    }
1954}
1955
1956fn first_moment_of_day(year: i32, month: u32, day: u32) -> NaiveDateTime {
1957    NaiveDate::from_ymd_opt(year, month, day)
1958        .unwrap()
1959        .and_hms_opt(0, 0, 0)
1960        .unwrap()
1961}
1962
1963fn precise_moment(
1964    year: i32,
1965    month: u32,
1966    day: u32,
1967    hour: u32,
1968    minute: u32,
1969    second: u32,
1970) -> NaiveDateTime {
1971    NaiveDate::from_ymd_opt(year, month, day)
1972        .unwrap()
1973        .and_hms_opt(hour, minute, second)
1974        .unwrap()
1975}