blob: f77ef3f5ff0b3c80b03538e7be8e05a742a36031 [file] [log] [blame]
//! ## Parsing English Dates
//!
//! I've always admired the ability of the GNU `date` command to
//! convert "English" expressions to dates and times with `date -d expr`.
//! `chrono-english` does similar expressions, although with extensions, so
//! that for instance you can specify both the day and the time "next friday 8pm".
//! No attempt at full natural language parsing is made - only a limited set of
//! patterns is supported.
//!
//! ## Supported Formats
//!
//! `chrono-english` does _absolute_ dates: ISO-like dates "2018-04-01" and the month name forms
//! "1 April 2018" and "April 1, 2018". (There's no ambiguity so both of these forms are fine)
//!
//! The informal "01/04/18" or American form "04/01/18" is supported.
//! There is a `Dialect` enum to specify what kind of date English you would like to speak.
//! Both short and long years are accepted in this form; short dates pivot between 1940 and 2040.
//!
//! Then there are are _relative_ dates like 'April 1' and '9/11' (this
//! if using `Dialect::Us`). The current year is assumed, but this can be modified by 'next'
//! and 'last'. For instance, it is now the 13th of March, 2018: 'April 1' and 'next April 1'
//! are in 2018; 'last April 1' is in 2017.
//!
//! Another relative form is simply a month name
//! like 'apr' or 'April' (case-insensitive, only first three letters significant) where the
//! day is assumed to be the 1st.
//!
//! A week-day works in the same way: 'friday' means this
//! coming Friday, relative to today. 'last Friday' is unambiguous,
//! but 'next Friday' has different meanings; in the US it means the same as 'Friday'
//! but otherwise it means the Friday of next week (plus 7 days)
//!
//! Date and time can be specified also by a number of time units. So "2 days", "3 hours".
//! Again, first three letters, but 'd','m' and 'y' are understood (so "3h"). We make
//! a distinction between _second_ intervals (seconds,minutes,hours,days,weeks) and _month_
//! intervals (months,years). Month intervals always give us the same date, if possible
//! But adding a month to "30 Jan" will give "28 Feb" or "29 Feb" depending if a leap year.
//!
//! Finally, dates may be followed by time. Either 'formal' like 18:03, with optional
//! second (like 18:03:40) or 'informal' like 6.03pm. So one gets "next friday 8pm' and so
//! forth.
//!
//! ## API
//!
//! There are two entry points: `parse_date_string` and `parse_duration`. The
//! first is given the date string, a `DateTime` from which relative dates and
//! times operate, and a dialect (either `Dialect::Uk` or `Dialect::Us`
//! currently.) The base time also specifies the desired timezone.
//!
//! ```ignore
//! extern crate chrono_english;
//! extern crate chrono;
//! use chrono_english::{parse_date_string,Dialect};
//!
//! use chrono::prelude::*;
//!
//! let date_time = parse_date_string("next friday 8pm", Local::now(), Dialect::Uk)?;
//! println!("{}",date_time.format("%c"));
//! ```
//!
//! There is a little command-line program `parse-date` in the `examples` folder which can be used to play
//! with these expressions.
//!
//! The other function, `parse_duration`, lets you access just the relative part
//! of a string like 'two days ago' or '12 hours'. If successful, returns an
//! `Interval`, which is a number of seconds, days, or months.
//!
//! ```
//! use chrono_english::{parse_duration,Interval};
//!
//! assert_eq!(parse_duration("15m ago").unwrap(), Interval::Seconds(-15 * 60));
//! ```
//!
extern crate chrono;
extern crate scanlex;
use chrono::prelude::*;
mod errors;
mod parser;
mod types;
use errors::*;
use types::*;
pub use errors::{DateError, DateResult};
pub use types::Interval;
#[derive(Clone, Copy, Debug)]
pub enum Dialect {
Uk,
Us,
}
pub fn parse_date_string<Tz: TimeZone>(
s: &str,
now: DateTime<Tz>,
dialect: Dialect,
) -> DateResult<DateTime<Tz>>
where
Tz::Offset: Copy,
{
let mut dp = parser::DateParser::new(s);
if let Dialect::Us = dialect {
dp = dp.american_date();
}
let d = dp.parse()?;
// we may have explicit hour:minute:sec
let tspec = match d.time {
Some(tspec) => tspec,
None => TimeSpec::new_empty(),
};
if tspec.offset.is_some() {
// return DateTime::fix()::parse_from_rfc3339(s);
}
let date_time = if let Some(dspec) = d.date {
dspec
.to_date_time(now, tspec, dp.american)
.or_err("bad date")?
} else {
// no date, time set for today's date
tspec.to_date_time(now.date()).or_err("bad time")?
};
Ok(date_time)
}
pub fn parse_duration(s: &str) -> DateResult<Interval> {
let mut dp = parser::DateParser::new(s);
let d = dp.parse()?;
if d.time.is_some() {
return date_result("unexpected time component");
}
// shouldn't happen, but.
if d.date.is_none() {
return date_result("could not parse date");
}
match d.date.unwrap() {
DateSpec::Absolute(_) => date_result("unexpected absolute date"),
DateSpec::FromName(_) => date_result("unexpected date component"),
DateSpec::Relative(skip) => Ok(skip.to_interval()),
}
}
#[cfg(test)]
mod tests {
use super::*;
const FMT_ISO: &str = "%+";
fn display(t: DateResult<DateTime<Utc>>) -> String {
t.unwrap().format(FMT_ISO).to_string()
}
#[test]
fn basics() {
let base = parse_date_string("2018-03-21 11:00", Utc::now(), Dialect::Uk).unwrap();
// Day of week - relative to today. May have a time part
assert_eq!(
display(parse_date_string("friday", base, Dialect::Uk)),
"2018-03-23T00:00:00+00:00"
);
assert_eq!(
display(parse_date_string("friday 10:30", base, Dialect::Uk)),
"2018-03-23T10:30:00+00:00"
);
assert_eq!(
display(parse_date_string("friday 8pm", base, Dialect::Uk)),
"2018-03-23T20:00:00+00:00"
);
// The day of week is the _next_ day after today, so "Tuesday" is the next Tuesday after Wednesday
assert_eq!(
display(parse_date_string("tues", base, Dialect::Uk)),
"2018-03-27T00:00:00+00:00"
);
// The expression 'next Monday' is ambiguous; in the US it means the day following (same as 'Monday')
// (This is how the `date` command interprets it)
assert_eq!(
display(parse_date_string("next mon", base, Dialect::Us)),
"2018-03-26T00:00:00+00:00"
);
// but otherwise it means the day in the next week..
assert_eq!(
display(parse_date_string("next mon", base, Dialect::Uk)),
"2018-04-02T00:00:00+00:00"
);
assert_eq!(
display(parse_date_string("last fri 9.30", base, Dialect::Uk)),
"2018-03-16T09:30:00+00:00"
);
// date expressed as month, day - relative to today. May have a time part
assert_eq!(
display(parse_date_string("9/11", base, Dialect::Us)),
"2018-09-11T00:00:00+00:00"
);
assert_eq!(
display(parse_date_string("last 9/11", base, Dialect::Us)),
"2017-09-11T00:00:00+00:00"
);
assert_eq!(
display(parse_date_string("last 9/11 9am", base, Dialect::Us)),
"2017-09-11T09:00:00+00:00"
);
assert_eq!(
display(parse_date_string("April 1 8.30pm", base, Dialect::Uk)),
"2018-04-01T20:30:00+00:00"
);
// advance by time unit from today
// without explicit time, use base time - otherwise override
assert_eq!(
display(parse_date_string("2d", base, Dialect::Uk)),
"2018-03-23T11:00:00+00:00"
);
assert_eq!(
display(parse_date_string("2d 03:00", base, Dialect::Uk)),
"2018-03-23T03:00:00+00:00"
);
assert_eq!(
display(parse_date_string("3 weeks", base, Dialect::Uk)),
"2018-04-11T11:00:00+00:00"
);
assert_eq!(
display(parse_date_string("3h", base, Dialect::Uk)),
"2018-03-21T14:00:00+00:00"
);
assert_eq!(
display(parse_date_string("6 months", base, Dialect::Uk)),
"2018-09-21T00:00:00+00:00"
);
assert_eq!(
display(parse_date_string("6 months ago", base, Dialect::Uk)),
"2017-09-21T00:00:00+00:00"
);
assert_eq!(
display(parse_date_string("3 hours ago", base, Dialect::Uk)),
"2018-03-21T08:00:00+00:00"
);
assert_eq!(
display(parse_date_string(" -3h", base, Dialect::Uk)),
"2018-03-21T08:00:00+00:00"
);
assert_eq!(
display(parse_date_string(" -3 month", base, Dialect::Uk)),
"2017-12-21T00:00:00+00:00"
);
// absolute date with year, month, day - formal ISO and informal UK or US
assert_eq!(
display(parse_date_string("2017-06-30", base, Dialect::Uk)),
"2017-06-30T00:00:00+00:00"
);
assert_eq!(
display(parse_date_string("30/06/17", base, Dialect::Uk)),
"2017-06-30T00:00:00+00:00"
);
assert_eq!(
display(parse_date_string("06/30/17", base, Dialect::Us)),
"2017-06-30T00:00:00+00:00"
);
// may be followed by time part, formal and informal
assert_eq!(
display(parse_date_string("2017-06-30 08:20:30", base, Dialect::Uk)),
"2017-06-30T08:20:30+00:00"
);
assert_eq!(
display(parse_date_string(
"2017-06-30 08:20:30 +02:00",
base,
Dialect::Uk
)),
"2017-06-30T06:20:30+00:00"
);
assert_eq!(
display(parse_date_string(
"2017-06-30 08:20:30 +0200",
base,
Dialect::Uk
)),
"2017-06-30T06:20:30+00:00"
);
assert_eq!(
display(parse_date_string("2017-06-30T08:20:30Z", base, Dialect::Uk)),
"2017-06-30T08:20:30+00:00"
);
assert_eq!(
display(parse_date_string("2017-06-30T08:20:30", base, Dialect::Uk)),
"2017-06-30T08:20:30+00:00"
);
assert_eq!(
display(parse_date_string("2017-06-30 8.20", base, Dialect::Uk)),
"2017-06-30T08:20:00+00:00"
);
assert_eq!(
display(parse_date_string("2017-06-30 8.30pm", base, Dialect::Uk)),
"2017-06-30T20:30:00+00:00"
);
assert_eq!(
display(parse_date_string("2017-06-30 8:30pm", base, Dialect::Uk)),
"2017-06-30T20:30:00+00:00"
);
assert_eq!(
display(parse_date_string("2017-06-30 2am", base, Dialect::Uk)),
"2017-06-30T02:00:00+00:00"
);
assert_eq!(
display(parse_date_string("30 June 2018", base, Dialect::Uk)),
"2018-06-30T00:00:00+00:00"
);
assert_eq!(
display(parse_date_string("June 30, 2018", base, Dialect::Uk)),
"2018-06-30T00:00:00+00:00"
);
assert_eq!(
display(parse_date_string("June 30, 2018", base, Dialect::Uk)),
"2018-06-30T00:00:00+00:00"
);
}
fn get_err(r: DateResult<Interval>) -> String {
r.err().unwrap().to_string()
}
#[test]
fn durations() {
assert_eq!(parse_duration("6h").unwrap(), Interval::Seconds(6 * 3600));
assert_eq!(
parse_duration("4 hours ago").unwrap(),
Interval::Seconds(-4 * 3600)
);
assert_eq!(parse_duration("5 min").unwrap(), Interval::Seconds(5 * 60));
assert_eq!(parse_duration("10m").unwrap(), Interval::Seconds(10 * 60));
assert_eq!(
parse_duration("15m ago").unwrap(),
Interval::Seconds(-15 * 60)
);
assert_eq!(parse_duration("1 day").unwrap(), Interval::Days(1));
assert_eq!(parse_duration("2 days ago").unwrap(), Interval::Days(-2));
assert_eq!(parse_duration("3 weeks").unwrap(), Interval::Days(21));
assert_eq!(parse_duration("2 weeks ago").unwrap(), Interval::Days(-14));
assert_eq!(parse_duration("1 month").unwrap(), Interval::Months(1));
assert_eq!(parse_duration("6 months").unwrap(), Interval::Months(6));
assert_eq!(parse_duration("8 years").unwrap(), Interval::Months(12 * 8));
// errors
assert_eq!(
get_err(parse_duration("2020-01-01")),
"unexpected absolute date"
);
assert_eq!(
get_err(parse_duration("2 days 15:00")),
"unexpected time component"
);
assert_eq!(
get_err(parse_duration("tuesday")),
"unexpected date component"
);
assert_eq!(
get_err(parse_duration("bananas")),
"expected week day or month name"
);
}
}