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::{DateError, DateResult};
86pub use types::Interval;
87
88#[derive(Clone, Copy, Debug)]
89pub enum Dialect {
90    Uk,
91    Us,
92}
93
94pub fn parse_date_string<Tz: TimeZone>(
95    s: &str,
96    now: DateTime<Tz>,
97    dialect: Dialect,
98) -> DateResult<DateTime<Tz>>
99where
100    Tz::Offset: Copy,
101{
102    let mut dp = parser::DateParser::new(s);
103    if let Dialect::Us = dialect {
104        dp = dp.american_date();
105    }
106    let d = dp.parse()?;
107
108    // we may have explicit hour:minute:sec
109    let tspec = match d.time {
110        Some(tspec) => tspec,
111        None => TimeSpec::new_empty(),
112    };
113    if tspec.offset.is_some() {
114        //   return DateTime::fix()::parse_from_rfc3339(s);
115    }
116    let date_time = if let Some(dspec) = d.date {
117        dspec
118            .to_date_time(now, tspec, dp.american)
119            .or_err("bad date")?
120    } else {
121        // no date, time set for today's date
122        tspec.to_date_time(now.date()).or_err("bad time")?
123    };
124    Ok(date_time)
125}
126
127pub fn parse_duration(s: &str) -> DateResult<Interval> {
128    let mut dp = parser::DateParser::new(s);
129    let d = dp.parse()?;
130
131    if d.time.is_some() {
132        return date_result("unexpected time component");
133    }
134
135    // shouldn't happen, but.
136    if d.date.is_none() {
137        return date_result("could not parse date");
138    }
139
140    match d.date.unwrap() {
141        DateSpec::Absolute(_) => date_result("unexpected absolute date"),
142        DateSpec::FromName(_) => date_result("unexpected date component"),
143        DateSpec::Relative(skip) => Ok(skip.to_interval()),
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    const FMT_ISO: &str = "%+";
152
153    fn display(t: DateResult<DateTime<Utc>>) -> String {
154        t.unwrap().format(FMT_ISO).to_string()
155    }
156
157    #[test]
158    fn basics() {
159        let base = parse_date_string("2018-03-21 11:00", Utc::now(), Dialect::Uk).unwrap();
160
161        // Day of week - relative to today. May have a time part
162        assert_eq!(
163            display(parse_date_string("friday", base, Dialect::Uk)),
164            "2018-03-23T00:00:00+00:00"
165        );
166        assert_eq!(
167            display(parse_date_string("friday 10:30", base, Dialect::Uk)),
168            "2018-03-23T10:30:00+00:00"
169        );
170        assert_eq!(
171            display(parse_date_string("friday 8pm", base, Dialect::Uk)),
172            "2018-03-23T20:00:00+00:00"
173        );
174
175        // The day of week is the _next_ day after today, so "Tuesday" is the next Tuesday after Wednesday
176        assert_eq!(
177            display(parse_date_string("tues", base, Dialect::Uk)),
178            "2018-03-27T00:00:00+00:00"
179        );
180
181        // The expression 'next Monday' is ambiguous; in the US it means the day following (same as 'Monday')
182        // (This is how the `date` command interprets it)
183        assert_eq!(
184            display(parse_date_string("next mon", base, Dialect::Us)),
185            "2018-03-26T00:00:00+00:00"
186        );
187        // but otherwise it means the day in the next week..
188        assert_eq!(
189            display(parse_date_string("next mon", base, Dialect::Uk)),
190            "2018-04-02T00:00:00+00:00"
191        );
192
193        assert_eq!(
194            display(parse_date_string("last fri 9.30", base, Dialect::Uk)),
195            "2018-03-16T09:30:00+00:00"
196        );
197
198        // date expressed as month, day - relative to today. May have a time part
199        assert_eq!(
200            display(parse_date_string("9/11", base, Dialect::Us)),
201            "2018-09-11T00:00:00+00:00"
202        );
203        assert_eq!(
204            display(parse_date_string("last 9/11", base, Dialect::Us)),
205            "2017-09-11T00:00:00+00:00"
206        );
207        assert_eq!(
208            display(parse_date_string("last 9/11 9am", base, Dialect::Us)),
209            "2017-09-11T09:00:00+00:00"
210        );
211        assert_eq!(
212            display(parse_date_string("April 1 8.30pm", base, Dialect::Uk)),
213            "2018-04-01T20:30:00+00:00"
214        );
215
216        // advance by time unit from today
217        // without explicit time, use base time - otherwise override
218        assert_eq!(
219            display(parse_date_string("2d", base, Dialect::Uk)),
220            "2018-03-23T11:00:00+00:00"
221        );
222        assert_eq!(
223            display(parse_date_string("2d 03:00", base, Dialect::Uk)),
224            "2018-03-23T03:00:00+00:00"
225        );
226        assert_eq!(
227            display(parse_date_string("3 weeks", base, Dialect::Uk)),
228            "2018-04-11T11:00:00+00:00"
229        );
230        assert_eq!(
231            display(parse_date_string("3h", base, Dialect::Uk)),
232            "2018-03-21T14:00:00+00:00"
233        );
234        assert_eq!(
235            display(parse_date_string("6 months", base, Dialect::Uk)),
236            "2018-09-21T00:00:00+00:00"
237        );
238        assert_eq!(
239            display(parse_date_string("6 months ago", base, Dialect::Uk)),
240            "2017-09-21T00:00:00+00:00"
241        );
242        assert_eq!(
243            display(parse_date_string("3 hours ago", base, Dialect::Uk)),
244            "2018-03-21T08:00:00+00:00"
245        );
246        assert_eq!(
247            display(parse_date_string(" -3h", base, Dialect::Uk)),
248            "2018-03-21T08:00:00+00:00"
249        );
250        assert_eq!(
251            display(parse_date_string(" -3 month", base, Dialect::Uk)),
252            "2017-12-21T00:00:00+00:00"
253        );
254
255        // absolute date with year, month, day - formal ISO and informal UK or US
256        assert_eq!(
257            display(parse_date_string("2017-06-30", base, Dialect::Uk)),
258            "2017-06-30T00:00:00+00:00"
259        );
260        assert_eq!(
261            display(parse_date_string("30/06/17", base, Dialect::Uk)),
262            "2017-06-30T00:00:00+00:00"
263        );
264        assert_eq!(
265            display(parse_date_string("06/30/17", base, Dialect::Us)),
266            "2017-06-30T00:00:00+00:00"
267        );
268
269        // may be followed by time part, formal and informal
270        assert_eq!(
271            display(parse_date_string("2017-06-30 08:20:30", base, Dialect::Uk)),
272            "2017-06-30T08:20:30+00:00"
273        );
274        assert_eq!(
275            display(parse_date_string(
276                "2017-06-30 08:20:30 +02:00",
277                base,
278                Dialect::Uk
279            )),
280            "2017-06-30T06:20:30+00:00"
281        );
282        assert_eq!(
283            display(parse_date_string(
284                "2017-06-30 08:20:30 +0200",
285                base,
286                Dialect::Uk
287            )),
288            "2017-06-30T06:20:30+00:00"
289        );
290        assert_eq!(
291            display(parse_date_string("2017-06-30T08:20:30Z", base, Dialect::Uk)),
292            "2017-06-30T08:20:30+00:00"
293        );
294        assert_eq!(
295            display(parse_date_string("2017-06-30T08:20:30", base, Dialect::Uk)),
296            "2017-06-30T08:20:30+00:00"
297        );
298        assert_eq!(
299            display(parse_date_string("2017-06-30 8.20", base, Dialect::Uk)),
300            "2017-06-30T08:20:00+00:00"
301        );
302        assert_eq!(
303            display(parse_date_string("2017-06-30 8.30pm", base, Dialect::Uk)),
304            "2017-06-30T20:30:00+00:00"
305        );
306        assert_eq!(
307            display(parse_date_string("2017-06-30 8:30pm", base, Dialect::Uk)),
308            "2017-06-30T20:30:00+00:00"
309        );
310        assert_eq!(
311            display(parse_date_string("2017-06-30 2am", base, Dialect::Uk)),
312            "2017-06-30T02:00:00+00:00"
313        );
314        assert_eq!(
315            display(parse_date_string("30 June 2018", base, Dialect::Uk)),
316            "2018-06-30T00:00:00+00:00"
317        );
318        assert_eq!(
319            display(parse_date_string("June 30, 2018", base, Dialect::Uk)),
320            "2018-06-30T00:00:00+00:00"
321        );
322        assert_eq!(
323            display(parse_date_string("June   30,    2018", base, Dialect::Uk)),
324            "2018-06-30T00:00:00+00:00"
325        );
326    }
327
328    fn get_err(r: DateResult<Interval>) -> String {
329        r.err().unwrap().to_string()
330    }
331
332    #[test]
333    fn durations() {
334        assert_eq!(parse_duration("6h").unwrap(), Interval::Seconds(6 * 3600));
335        assert_eq!(
336            parse_duration("4 hours ago").unwrap(),
337            Interval::Seconds(-4 * 3600)
338        );
339        assert_eq!(parse_duration("5 min").unwrap(), Interval::Seconds(5 * 60));
340        assert_eq!(parse_duration("10m").unwrap(), Interval::Seconds(10 * 60));
341        assert_eq!(
342            parse_duration("15m ago").unwrap(),
343            Interval::Seconds(-15 * 60)
344        );
345
346        assert_eq!(parse_duration("1 day").unwrap(), Interval::Days(1));
347        assert_eq!(parse_duration("2 days ago").unwrap(), Interval::Days(-2));
348        assert_eq!(parse_duration("3 weeks").unwrap(), Interval::Days(21));
349        assert_eq!(parse_duration("2 weeks ago").unwrap(), Interval::Days(-14));
350
351        assert_eq!(parse_duration("1 month").unwrap(), Interval::Months(1));
352        assert_eq!(parse_duration("6 months").unwrap(), Interval::Months(6));
353        assert_eq!(parse_duration("8 years").unwrap(), Interval::Months(12 * 8));
354
355        // errors
356        assert_eq!(
357            get_err(parse_duration("2020-01-01")),
358            "unexpected absolute date"
359        );
360        assert_eq!(
361            get_err(parse_duration("2 days 15:00")),
362            "unexpected time component"
363        );
364        assert_eq!(
365            get_err(parse_duration("tuesday")),
366            "unexpected date component"
367        );
368        assert_eq!(
369            get_err(parse_duration("bananas")),
370            "expected week day or month name"
371        );
372    }
373}