chrono_english/
lib.rs

1//! ## Parsing English Dates
2//!
3//! I've always admired the ability of the GNU `date` command to
4//! convert "English" expressions to dates and times with `date -d expr`.
5//! `chrono-english` does similar expressions, although with extensions, so
6//! that for instance you can specify both the day and the time "next friday 8pm".
7//! No attempt at full natural language parsing is made - only a limited set of
8//! patterns is supported.
9//!
10//! ## Supported Formats
11//!
12//! `chrono-english` does _absolute_ dates:  ISO-like dates "2018-04-01" and the month name forms
13//! "1 April 2018" and "April 1, 2018". (There's no ambiguity so both of these forms are fine)
14//!
15//! The informal "01/04/18" or American form "04/01/18" is supported.
16//! There is a `Dialect` enum to specify what kind of date English you would like to speak.
17//! Both short and long years are accepted in this form; short dates pivot between 1940 and 2040.
18//!
19//! Then there are are _relative_ dates like 'April 1' and '9/11' (this
20//! if using `Dialect::Us`). The current year is assumed, but this can be modified by 'next'
21//! and 'last'. For instance, it is now the 13th of March, 2018: 'April 1' and 'next April 1'
22//! are in 2018; 'last April 1' is in 2017.
23//!
24//! Another relative form is simply a month name
25//! like 'apr' or 'April' (case-insensitive, only first three letters significant) where the
26//! day is assumed to be the 1st.
27//!
28//! A week-day works in the same way: 'friday' means this
29//! coming Friday, relative to today. 'last Friday' is unambiguous,
30//! but 'next Friday' has different meanings; in the US it means the same as 'Friday'
31//! but otherwise it means the Friday of next week (plus 7 days)
32//!
33//! Date and time can be specified also by a number of time units. So "2 days", "3 hours".
34//! Again, first three letters, but 'd','m' and 'y' are understood (so "3h"). We make
35//! a distinction between _second_ intervals (seconds,minutes,hours,days,weeks) and _month_
36//! intervals (months,years).  Month intervals always give us the same date, if possible
37//! But adding a month to "30 Jan" will give "28 Feb" or "29 Feb" depending if a leap year.
38//!
39//! Finally, dates may be followed by time. Either 'formal' like 18:03, with optional
40//! second (like 18:03:40) or 'informal' like 6.03pm. So one gets "next friday 8pm' and so
41//! forth.
42//!
43//! ## API
44//!
45//! There are two entry points: `parse_date_string` and `parse_duration`. The
46//! first is given the date string, a `DateTime` from which relative dates and
47//! times operate, and a dialect (either `Dialect::Uk` or `Dialect::Us`
48//! currently.) The base time also specifies the desired timezone.
49//!
50//! ```ignore
51//! extern crate chrono_english;
52//! extern crate chrono;
53//! use chrono_english::{parse_date_string,Dialect};
54//!
55//! use chrono::prelude::*;
56//!
57//! let date_time = parse_date_string("next friday 8pm", Local::now(), Dialect::Uk)?;
58//! println!("{}",date_time.format("%c"));
59//! ```
60//!
61//! There is a little command-line program `parse-date` in the `examples` folder which can be used to play
62//! with these expressions.
63//!
64//! The other function, `parse_duration`, lets you access just the relative part
65//! of a string like 'two days ago' or '12 hours'. If successful, returns an
66//! `Interval`, which is a number of seconds, days, or months.
67//!
68//! ```
69//! use chrono_english::{parse_duration,Interval};
70//!
71//! assert_eq!(parse_duration("15m ago").unwrap(), Interval::Seconds(-15 * 60));
72//! ```
73//!
74
75extern crate chrono;
76extern crate scanlex;
77use chrono::prelude::*;
78
79mod errors;
80mod parser;
81mod types;
82use errors::*;
83use types::*;
84
85pub use errors::{date_error, date_result};
86pub use errors::{DateError, DateResult};
87pub use types::Interval;
88
89#[derive(Debug, Hash, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
90pub enum Dialect {
91    Uk,
92    Us,
93}
94
95pub fn parse_date_string<Tz: TimeZone>(
96    s: &str,
97    now: DateTime<Tz>,
98    dialect: Dialect,
99) -> DateResult<DateTime<Tz>>
100where
101    Tz::Offset: Copy,
102{
103    let mut dp = parser::DateParser::new(s);
104    if let Dialect::Us = dialect {
105        dp = dp.american_date();
106    }
107    let d = dp.parse()?;
108
109    // we may have explicit hour:minute:sec
110    let tspec = d.time.unwrap_or_else(|| TimeSpec::new_empty());
111    if tspec.offset.is_some() {
112        //   return DateTime::fix()::parse_from_rfc3339(s);
113    }
114    let date_time = if let Some(dspec) = d.date {
115        dspec
116            .to_date_time(now, tspec, dp.american)
117            .or_err("bad date")?
118    } else {
119        // no date, time set for today's date
120        tspec.to_date_time(now).or_err("bad time")?
121    };
122    Ok(date_time)
123}
124
125pub fn parse_duration(s: &str) -> DateResult<Interval> {
126    let mut dp = parser::DateParser::new(s);
127    let d = dp.parse()?;
128
129    if d.time.is_some() {
130        return date_result("unexpected time component");
131    }
132
133    // shouldn't happen, but.
134    if d.date.is_none() {
135        return date_result("could not parse date");
136    }
137
138    match d.date.unwrap() {
139        DateSpec::Absolute(_) => date_result("unexpected absolute date"),
140        DateSpec::FromName(_) => date_result("unexpected date component"),
141        DateSpec::Relative(skip) => Ok(skip.to_interval()),
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    const FMT_ISO: &str = "%+";
150
151    fn display(t: DateResult<DateTime<Utc>>) -> String {
152        t.unwrap().format(FMT_ISO).to_string()
153    }
154
155    #[test]
156    fn basics() {
157        let base = parse_date_string("2018-03-21 11:00", Utc::now(), Dialect::Uk).unwrap();
158
159        // Day of week - relative to today. May have a time part
160        assert_eq!(
161            display(parse_date_string("friday", base, Dialect::Uk)),
162            "2018-03-23T00:00:00+00:00"
163        );
164        assert_eq!(
165            display(parse_date_string("friday 10:30", base, Dialect::Uk)),
166            "2018-03-23T10:30:00+00:00"
167        );
168        assert_eq!(
169            display(parse_date_string("friday 8pm", base, Dialect::Uk)),
170            "2018-03-23T20:00:00+00:00"
171        );
172
173        // The day of week is the _next_ day after today, so "Tuesday" is the next Tuesday after Wednesday
174        assert_eq!(
175            display(parse_date_string("tues", base, Dialect::Uk)),
176            "2018-03-27T00:00:00+00:00"
177        );
178
179        // The expression 'next Monday' is ambiguous; in the US it means the day following (same as 'Monday')
180        // (This is how the `date` command interprets it)
181        assert_eq!(
182            display(parse_date_string("next mon", base, Dialect::Us)),
183            "2018-03-26T00:00:00+00:00"
184        );
185        // but otherwise it means the day in the next week..
186        assert_eq!(
187            display(parse_date_string("next mon", base, Dialect::Uk)),
188            "2018-04-02T00:00:00+00:00"
189        );
190
191        assert_eq!(
192            display(parse_date_string("last fri 9.30", base, Dialect::Uk)),
193            "2018-03-16T09:30:00+00:00"
194        );
195
196        // date expressed as month, day - relative to today. May have a time part
197        assert_eq!(
198            display(parse_date_string("9/11", base, Dialect::Us)),
199            "2018-09-11T00:00:00+00:00"
200        );
201        assert_eq!(
202            display(parse_date_string("last 9/11", base, Dialect::Us)),
203            "2017-09-11T00:00:00+00:00"
204        );
205        assert_eq!(
206            display(parse_date_string("last 9/11 9am", base, Dialect::Us)),
207            "2017-09-11T09:00:00+00:00"
208        );
209        assert_eq!(
210            display(parse_date_string("April 1 8.30pm", base, Dialect::Uk)),
211            "2018-04-01T20:30:00+00:00"
212        );
213
214        // advance by time unit from today
215        // without explicit time, use base time - otherwise override
216        assert_eq!(
217            display(parse_date_string("2d", base, Dialect::Uk)),
218            "2018-03-23T11:00:00+00:00"
219        );
220        assert_eq!(
221            display(parse_date_string("2d 03:00", base, Dialect::Uk)),
222            "2018-03-23T03:00:00+00:00"
223        );
224        assert_eq!(
225            display(parse_date_string("3 weeks", base, Dialect::Uk)),
226            "2018-04-11T11:00:00+00:00"
227        );
228        assert_eq!(
229            display(parse_date_string("3h", base, Dialect::Uk)),
230            "2018-03-21T14:00:00+00:00"
231        );
232        assert_eq!(
233            display(parse_date_string("6 months", base, Dialect::Uk)),
234            "2018-09-21T00:00:00+00:00"
235        );
236        assert_eq!(
237            display(parse_date_string("6 months ago", base, Dialect::Uk)),
238            "2017-09-21T00:00:00+00:00"
239        );
240        assert_eq!(
241            display(parse_date_string("3 hours ago", base, Dialect::Uk)),
242            "2018-03-21T08:00:00+00:00"
243        );
244        assert_eq!(
245            display(parse_date_string(" -3h", base, Dialect::Uk)),
246            "2018-03-21T08:00:00+00:00"
247        );
248        assert_eq!(
249            display(parse_date_string(" -3 month", base, Dialect::Uk)),
250            "2017-12-21T00:00:00+00:00"
251        );
252
253        // absolute date with year, month, day - formal ISO and informal UK or US
254        assert_eq!(
255            display(parse_date_string("2017-06-30", base, Dialect::Uk)),
256            "2017-06-30T00:00:00+00:00"
257        );
258        assert_eq!(
259            display(parse_date_string("30/06/17", base, Dialect::Uk)),
260            "2017-06-30T00:00:00+00:00"
261        );
262        assert_eq!(
263            display(parse_date_string("06/30/17", base, Dialect::Us)),
264            "2017-06-30T00:00:00+00:00"
265        );
266
267        // may be followed by time part, formal and informal
268        assert_eq!(
269            display(parse_date_string("2017-06-30 08:20:30", base, Dialect::Uk)),
270            "2017-06-30T08:20:30+00:00"
271        );
272        assert_eq!(
273            display(parse_date_string(
274                "2017-06-30 08:20:30 +02:00",
275                base,
276                Dialect::Uk
277            )),
278            "2017-06-30T06:20:30+00:00"
279        );
280        assert_eq!(
281            display(parse_date_string(
282                "2017-06-30 08:20:30 +0200",
283                base,
284                Dialect::Uk
285            )),
286            "2017-06-30T06:20:30+00:00"
287        );
288        assert_eq!(
289            display(parse_date_string("2017-06-30T08:20:30Z", base, Dialect::Uk)),
290            "2017-06-30T08:20:30+00:00"
291        );
292        assert_eq!(
293            display(parse_date_string("2017-06-30T08:20:30", base, Dialect::Uk)),
294            "2017-06-30T08:20:30+00:00"
295        );
296        assert_eq!(
297            display(parse_date_string("2017-06-30 8.20", base, Dialect::Uk)),
298            "2017-06-30T08:20:00+00:00"
299        );
300        assert_eq!(
301            display(parse_date_string("2017-06-30 8.30pm", base, Dialect::Uk)),
302            "2017-06-30T20:30:00+00:00"
303        );
304        assert_eq!(
305            display(parse_date_string("2017-06-30 8:30pm", base, Dialect::Uk)),
306            "2017-06-30T20:30:00+00:00"
307        );
308        assert_eq!(
309            display(parse_date_string("2017-06-30 2am", base, Dialect::Uk)),
310            "2017-06-30T02:00:00+00:00"
311        );
312        assert_eq!(
313            display(parse_date_string("30 June 2018", base, Dialect::Uk)),
314            "2018-06-30T00:00:00+00:00"
315        );
316        assert_eq!(
317            display(parse_date_string("June 30, 2018", base, Dialect::Uk)),
318            "2018-06-30T00:00:00+00:00"
319        );
320        assert_eq!(
321            display(parse_date_string("June   30,    2018", base, Dialect::Uk)),
322            "2018-06-30T00:00:00+00:00"
323        );
324    }
325
326    fn get_err(r: DateResult<Interval>) -> String {
327        r.err().unwrap().to_string()
328    }
329
330    #[test]
331    fn durations() {
332        assert_eq!(parse_duration("6h").unwrap(), Interval::Seconds(6 * 3600));
333        assert_eq!(
334            parse_duration("4 hours ago").unwrap(),
335            Interval::Seconds(-4 * 3600)
336        );
337        assert_eq!(parse_duration("5 min").unwrap(), Interval::Seconds(5 * 60));
338        assert_eq!(parse_duration("10m").unwrap(), Interval::Seconds(10 * 60));
339        assert_eq!(
340            parse_duration("15m ago").unwrap(),
341            Interval::Seconds(-15 * 60)
342        );
343
344        assert_eq!(parse_duration("1 day").unwrap(), Interval::Days(1));
345        assert_eq!(parse_duration("2 days ago").unwrap(), Interval::Days(-2));
346        assert_eq!(parse_duration("3 weeks").unwrap(), Interval::Days(21));
347        assert_eq!(parse_duration("2 weeks ago").unwrap(), Interval::Days(-14));
348
349        assert_eq!(parse_duration("1 month").unwrap(), Interval::Months(1));
350        assert_eq!(parse_duration("6 months").unwrap(), Interval::Months(6));
351        assert_eq!(parse_duration("8 years").unwrap(), Interval::Months(12 * 8));
352
353        // errors
354        assert_eq!(
355            get_err(parse_duration("2020-01-01")),
356            "unexpected absolute date"
357        );
358        assert_eq!(
359            get_err(parse_duration("2 days 15:00")),
360            "unexpected time component"
361        );
362        assert_eq!(
363            get_err(parse_duration("tuesday")),
364            "unexpected date component"
365        );
366        assert_eq!(
367            get_err(parse_duration("bananas")),
368            "expected week day or month name"
369        );
370    }
371}