chrono-english-0.1.7/.cargo_vcs_info.json0000644000000001360000000000100137600ustar { "git": { "sha1": "176e1d3669e418edb7f2ec9e7734991e3ca12ea9" }, "path_in_vcs": "" }chrono-english-0.1.7/.gitignore000064400000000000000000000000560072674642500145710ustar 00000000000000scratch/ /target/ **/*.rs.bk Cargo.lock idea/ chrono-english-0.1.7/Cargo.lock0000644000000050350000000000100117360ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "chrono" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" dependencies = [ "num-integer", "num-traits", "time", ] [[package]] name = "chrono-english" version = "0.1.7" dependencies = [ "chrono", "lapp", "scanlex", ] [[package]] name = "lapp" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44758669f6f88b6dddac3f833f234d5128530108b5738932a166eb69660faea8" [[package]] name = "libc" version = "0.2.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d" [[package]] name = "num-integer" version = "0.1.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" dependencies = [ "num-traits", ] [[package]] name = "num-traits" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" [[package]] name = "redox_syscall" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" [[package]] name = "scanlex" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "088c5d71572124929ea7549a8ce98e1a6fd33d0a38367b09027b382e67c033db" [[package]] name = "time" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" dependencies = [ "libc", "redox_syscall", "winapi", ] [[package]] name = "winapi" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" chrono-english-0.1.7/Cargo.toml0000644000000016330000000000100117610ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] name = "chrono-english" version = "0.1.7" authors = ["steve donovan "] description = "parses simple English dates, inspired by Linux date command" documentation = "https://docs.rs/chrono-english" readme = "readme.md" license = "MIT" repository = "https://github.com/stevedonovan/chrono-english.git" [dependencies.chrono] version = "0.4" [dependencies.scanlex] version = "0.1.2" [dev-dependencies.lapp] version = "0.3" chrono-english-0.1.7/Cargo.toml.orig000064400000000000000000000006630072674642500154740ustar 00000000000000[package] name = "chrono-english" version = "0.1.7" authors = ["steve donovan "] description = "parses simple English dates, inspired by Linux date command" documentation = "https://docs.rs/chrono-english" repository = "https://github.com/stevedonovan/chrono-english.git" readme = "readme.md" license="MIT" [dependencies] chrono = { version = "0.4" } scanlex = "0.1.2" [dev-dependencies] lapp = "0.3" chrono-english-0.1.7/LICENSE.txt000064400000000000000000000020700072674642500144220ustar 00000000000000The MIT License (MIT) Copyright (c) 2018 Steve Donovan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. chrono-english-0.1.7/examples/parse-date.rs000064400000000000000000000032460072674642500170160ustar 00000000000000extern crate chrono_english; use chrono_english::{parse_date_string, Dialect}; extern crate chrono; use chrono::prelude::*; extern crate lapp; use std::error::Error; use std::fmt::Display; type BoxResult = Result>; const USAGE: &str = " Parsing Dates in English -a, --american informal dates are like 9/11 not 20/03 -u, --utc evaluate in UTC, not Local timezone (string) the date (default now) the base for relative dates "; const FMT_C: &str = "%c %z"; const FMT_ISO: &str = "%+"; fn parse_and_compare( datestr: &str, basestr: &str, now: DateTime, dialect: Dialect, ) -> BoxResult<()> where Tz::Offset: Display, Tz::Offset: Copy, { let def = basestr == "now"; let base = parse_date_string(basestr, now, dialect)?; let date_time = parse_date_string(&datestr, base, dialect)?; if !def { println!("base {} ({})", base.format(FMT_C), base.format(FMT_ISO)); } println!( "calc {} ({})", date_time.format(FMT_C), date_time.format(FMT_ISO) ); Ok(()) } fn run() -> BoxResult<()> { let args = lapp::parse_args(USAGE); let utc = args.get_bool("utc"); let datestr = args.get_string("date"); let basestr = args.get_string("base"); let dialect = if args.get_bool("american") { Dialect::Us } else { Dialect::Uk }; if utc { parse_and_compare(&datestr, &basestr, Utc::now(), dialect)?; } else { parse_and_compare(&datestr, &basestr, Local::now(), dialect)?; } Ok(()) } fn main() { if let Err(e) = run() { eprintln!("error: {}", e); std::process::exit(1); } } chrono-english-0.1.7/readme.md000064400000000000000000000065750072674642500143740ustar 00000000000000# 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), _day_ intervals (days,weeks) and _month_ intervals (months,years). Second intervals are not followed by a time, but day and month intervals can be. Without a time, a day interval has the same time as the base time (which defaults to 'now') 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 is exactly one entry point, which 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. ```rust extern crate chrono_english; use chrono_english::{parse_date_string,Dialect}; extern crate chrono; 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 `examples` which can be used to play with these expressions: ``` $ alias p='cargo run --quiet --example parse-date --' $ p 'next April' base Wed Mar 14 20:10:37 2018 +0200 calc Sun Apr 1 00:00:00 2018 +0200 $ p '20/03/18 12:04' base Wed Mar 14 20:12:44 2018 +0200 calc Tue Mar 20 12:04:00 2018 +0200 $ p '9/11/01' --american base Wed Mar 14 20:13:08 2018 +0200 calc Tue Sep 11 00:00:00 2001 +0200 $ p 'next fri 8pm' '2018-03-14' base Wed Mar 14 00:00:00 2018 +0200 calc Fri Mar 16 20:00:00 2018 +0200 ``` chrono-english-0.1.7/src/errors.rs000064400000000000000000000023120072674642500152470ustar 00000000000000use scanlex::ScanError; use std::error::Error; use std::fmt; #[derive(Debug)] pub struct DateError { details: String, } impl fmt::Display for DateError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.details) } } impl Error for DateError {} pub type DateResult = Result; pub fn date_error(msg: &str) -> DateError { DateError { details: msg.into(), } } pub fn date_result(msg: &str) -> DateResult { Err(date_error(msg).into()) } impl From for DateError { fn from(err: ScanError) -> DateError { date_error(&err.to_string()) } } /// This trait maps optional values onto `DateResult` pub trait OrErr { /// use when the error message is always a simple string fn or_err(self, msg: &str) -> DateResult; /// use when the message needs to be constructed fn or_then_err String>(self, fun: C) -> DateResult; } impl OrErr for Option { fn or_err(self, msg: &str) -> DateResult { self.ok_or(date_error(msg)) } fn or_then_err String>(self, fun: C) -> DateResult { self.ok_or_else(|| date_error(&fun())) } } chrono-english-0.1.7/src/lib.rs000064400000000000000000000315200072674642500145040ustar 00000000000000//! ## 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( s: &str, now: DateTime, dialect: Dialect, ) -> DateResult> 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 { 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>) -> 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) -> 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" ); } } chrono-english-0.1.7/src/parser.rs000064400000000000000000000310560072674642500152360ustar 00000000000000use errors::*; use scanlex::{Scanner, Token}; use types::*; // when we parse dates, there's often a bit of time parsed.. #[derive(Clone, Copy, Debug)] enum TimeKind { Formal, Informal, AmPm(bool), Unknown, } pub struct DateParser<'a> { s: Scanner<'a>, direct: Direction, maybe_time: Option<(u32, TimeKind)>, pub american: bool, // 9/11, not 20/03 } impl<'a> DateParser<'a> { pub fn new(text: &'a str) -> DateParser<'a> { DateParser { s: Scanner::new(text).no_float(), direct: Direction::Here, maybe_time: None, american: false, } } pub fn american_date(mut self) -> DateParser<'a> { self.american = true; self } fn iso_date(&mut self, y: u32) -> DateResult { let month = self.s.get_int::()?; self.s.get_ch_matching(&['-'])?; let day = self.s.get_int::()?; Ok(DateSpec::absolute(y, month, day)) } fn informal_date(&mut self, day_or_month: u32) -> DateResult { let month_or_day = self.s.get_int::()?; let (d, m) = if self.american { (month_or_day, day_or_month) } else { (day_or_month, month_or_day) }; Ok(if self.s.peek() == '/' { self.s.get(); let y = self.s.get_int::()?; let y = if y < 100 { // pivot (1940, 2040) if y > 40 { 1900 + y } else { 2000 + y } } else { y }; DateSpec::absolute(y, m, d) } else { DateSpec::FromName(ByName::from_day_month(d, m, self.direct)) }) } fn parse_date(&mut self) -> DateResult> { let mut t = self.s.next().or_err("empty date string")?; let sign = if t.is_char() && t.as_char().unwrap() == '-' { true } else { false }; if sign { t = self.s.next().or_err("nothing after '-'")?; } if let Some(name) = t.as_iden() { let shortcut = match name { "now" => Some(0), "today" => Some(0), "yesterday" => Some(-1), "tomorrow" => Some(1), _ => None, }; if let Some(skip) = shortcut { return Ok(Some(DateSpec::skip(time_unit("day").unwrap(), skip))); } else // maybe next or last? if let Some(d) = Direction::from_name(&name) { self.direct = d; } } if self.direct != Direction::Here { t = self.s.next().or_err("nothing after last/next")?; } Ok(match t { Token::Iden(ref name) => { let name = name.to_lowercase(); // maybe weekday or month name? if let Some(by_name) = ByName::from_name(&name, self.direct) { // however, MONTH _might_ be followed by DAY, YEAR if let Some(month) = by_name.as_month() { let t = self.s.get(); if t.is_integer() { let day = t.to_int_result::()?; return Ok(Some(if self.s.peek() == ',' { self.s.get_char()?; // eat ',' let year = self.s.get_int::()?; DateSpec::absolute(year, month, day) } else { // MONTH DAY is like DAY MONTH (tho no time!) DateSpec::from_day_month(day, month, self.direct) })); } } Some(DateSpec::FromName(by_name)) } else { return date_result("expected week day or month name"); } } Token::Int(_) => { let n = t.to_int_result::()?; let t = self.s.get(); if t.finished() { // must be a year... return Ok(Some(DateSpec::absolute(n, 1, 1))); } match t { Token::Iden(ref name) => { let day = n; let name = name.to_lowercase(); if let Some(month) = month_name(&name) { if let Ok(year) = self.s.get_int::() { // 4 July 2017 Some(DateSpec::absolute(year, month, day)) } else { // 4 July Some(DateSpec::from_day_month(day, month, self.direct)) } } else if let Some(u) = time_unit(&name) { // '2 days' let mut n = n as i32; if sign { n = -n; } else { let t = self.s.get(); let got_ago = if let Some(name) = t.as_iden() { if name == "ago" { n = -n; true } else { return date_result("only expected 'ago'"); } } else { false }; if !got_ago { if let Some(h) = t.to_integer() { self.maybe_time = Some((h as u32, TimeKind::Unknown)); } } } Some(DateSpec::skip(u, n)) } else if name == "am" || name == "pm" { self.maybe_time = Some((n, TimeKind::AmPm(name == "pm"))); None } else { return date_result("expected month or time unit"); } } Token::Char(ch) => match ch { '-' => Some(self.iso_date(n)?), '/' => Some(self.informal_date(n)?), ':' | '.' => { let kind = if ch == ':' { TimeKind::Formal } else { TimeKind::Informal }; self.maybe_time = Some((n, kind)); None } _ => return date_result(&format!("unexpected char {:?}", ch)), }, _ => return date_result(&format!("unexpected token {:?}", t)), } } _ => return date_result(&format!("not expected token {:?}", t)), }) } fn formal_time(&mut self, hour: u32) -> DateResult { let min = self.s.get_int::()?; // minute may be followed by [:secs][am|pm] let mut tnext = None; let sec = if let Some(t) = self.s.next() { if let Some(ch) = t.as_char() { if ch != ':' { return date_result("expecting ':'"); } self.s.get_int::()? } else { tnext = Some(t); 0 } } else { 0 }; // we found seconds, look ahead if tnext.is_none() { tnext = self.s.next(); } let micros = if let Some(Some('.')) = tnext.as_ref().map(|t| t.as_char()) { let frac = self.s.grab_while(char::is_numeric); if frac.is_empty() { return date_result("expected fractional second after '.'"); } let frac = "0.".to_owned() + &frac; let micros_f = frac.parse::().unwrap() * 1.0e6; tnext = self.s.next(); micros_f as u32 } else { 0 }; if tnext.is_none() { Ok(TimeSpec::new(hour, min, sec, micros)) } else { let tok = tnext.as_ref().unwrap(); if let Some(ch) = tok.as_char() { let expecting_offset = match ch { '+' | '-' => true, _ => return date_result("expected +/- before timezone"), }; let offset = if expecting_offset { let h = self.s.get_int::()?; let (h, m) = if self.s.peek() == ':' { // 02:00 self.s.nextch(); (h, self.s.get_int::()?) } else { // 0030 .... let hh = h; let h = hh / 100; let m = hh % 100; (h, m) }; let res = 60 * (m + 60 * h); (res as i64) * if ch == '-' { -1 } else { 1 } } else { 0 }; Ok(TimeSpec::new_with_offset(hour, min, sec, offset, micros)) } else if let Some(id) = tok.as_iden() { if id == "Z" { Ok(TimeSpec::new_with_offset(hour, min, sec, 0, micros)) } else { // am or pm let hour = DateParser::am_pm(&id, hour)?; Ok(TimeSpec::new(hour, min, sec, micros)) } } else { Ok(TimeSpec::new(hour, min, sec, micros)) } } } fn informal_time(&mut self, hour: u32) -> DateResult { let min = self.s.get_int::()?; let hour = if let Some(t) = self.s.next() { let name = t.to_iden_result()?; DateParser::am_pm(&name, hour)? } else { hour }; Ok(TimeSpec::new(hour, min, 0, 0)) } fn am_pm(name: &str, mut hour: u32) -> DateResult { if name == "pm" { hour += 12; } else if name != "am" { return date_result("expected am or pm"); } Ok(hour) } fn hour_time(name: &str, hour: u32) -> DateResult { Ok(TimeSpec::new(DateParser::am_pm(name, hour)?, 0, 0, 0)) } fn parse_time(&mut self) -> DateResult> { // here the date parser looked ahead and saw an hour followed by some separator if let Some(hour_sep) = self.maybe_time { // didn't see a separator, so look... let (h, mut kind) = hour_sep; if let TimeKind::Unknown = kind { kind = match self.s.get_char()? { ':' => TimeKind::Formal, '.' => TimeKind::Informal, ch => return date_result(&format!("expected : or ., not {}", ch)), }; } Ok(Some(match kind { TimeKind::Formal => self.formal_time(h)?, TimeKind::Informal => self.informal_time(h)?, TimeKind::AmPm(is_pm) => DateParser::hour_time(if is_pm { "pm" } else { "am" }, h)?, TimeKind::Unknown => unreachable!(), })) } else { // no lookahead... if self.s.peek() == 'T' { self.s.nextch(); } let t = self.s.get(); if t.finished() { return Ok(None); } let hour = t.to_int_result::()?; Ok(Some(match self.s.get() { Token::Char(ch) => match ch { ':' => self.formal_time(hour)?, '.' => self.informal_time(hour)?, ch => return date_result(&format!("unexpected char {:?}", ch)), }, Token::Iden(name) => DateParser::hour_time(&name, hour)?, t => return date_result(&format!("unexpected token {:?}", t)), })) } } pub fn parse(&mut self) -> DateResult { let date = self.parse_date()?; let time = self.parse_time()?; Ok(DateTimeSpec { date: date, time: time, }) } } chrono-english-0.1.7/src/types.rs000064400000000000000000000311020072674642500150760ustar 00000000000000use chrono::prelude::*; use chrono::Duration; // implements next/last direction in expressions like 'next friday' and 'last 4 july' #[derive(Debug, Clone, Copy, PartialEq)] pub enum Direction { Next, Last, Here, } impl Direction { pub fn from_name(s: &str) -> Option { use Direction::*; match s { "next" => Some(Next), "last" => Some(Last), _ => None, } } } // this is a day-month with direction, like 'next 10 Dec' #[derive(Debug)] pub struct YearDate { pub direct: Direction, pub month: u32, pub day: u32, } // for expressions like 'friday' and 'July' modifiable with next/last #[derive(Debug)] pub struct NamedDate { pub direct: Direction, pub unit: u32, } impl NamedDate { pub fn new(direct: Direction, unit: u32) -> NamedDate { NamedDate { direct: direct, unit: unit, } } } // all expressions modifiable with next/last; 'fri', 'jul', '5 may'. #[derive(Debug)] pub enum ByName { WeekDay(NamedDate), MonthName(NamedDate), DayMonth(YearDate), } fn add_days(base: DateTime, days: i64) -> Option> { base.checked_add_signed(Duration::days(days)) } //fn next_last_direction(date: Date, base: Date, direct: Direction) -> Option { fn next_last_direction(date: T, base: T, direct: Direction) -> Option { let mut res = None; if date > base { if direct == Direction::Last { res = Some(-1); } } else if date < base { if direct == Direction::Next { res = Some(1) } } res } impl ByName { pub fn from_name(s: &str, direct: Direction) -> Option { Some(if let Some(wd) = week_day(s) { ByName::WeekDay(NamedDate::new(direct, wd)) } else if let Some(mn) = month_name(s) { ByName::MonthName(NamedDate::new(direct, mn)) } else { return None; }) } pub fn as_month(&self) -> Option { match *self { ByName::MonthName(ref nd) => Some(nd.unit), _ => None, } } pub fn from_day_month(d: u32, m: u32, direct: Direction) -> ByName { ByName::DayMonth(YearDate { direct: direct, day: d, month: m, }) } pub fn to_date_time( self, base: DateTime, ts: TimeSpec, american: bool, ) -> Option> where ::Offset: Copy, { let this_year = base.year(); match self { ByName::WeekDay(mut nd) => { // a plain 'Friday' means the same as 'next Friday'. // an _explicit_ 'next Friday' has dialect-dependent meaning! // In UK English, it means 'Friday of next week', // but in US English, just the next Friday let mut extra_week = 0; match nd.direct { Direction::Here => nd.direct = Direction::Next, Direction::Next => { if !american { extra_week = 7; } } _ => (), }; let this_day = base.weekday().num_days_from_monday() as i64; let that_day = nd.unit as i64; let diff_days = that_day - this_day; let mut date = add_days(base, diff_days)?; if let Some(correct) = next_last_direction(date, base, nd.direct) { date = add_days(date, 7 * correct as i64)?; } if extra_week > 0 { date = add_days(date, extra_week)?; } if diff_days == 0 { // same day - comparing times will determine which way we swing... let base_time = base.time(); let this_time = NaiveTime::from_hms(ts.hour, ts.min, ts.sec); if let Some(correct) = next_last_direction(this_time, base_time, nd.direct) { date = add_days(date, 7 * correct as i64)?; } } ts.to_date_time(date.date()) } ByName::MonthName(nd) => { let mut date = base.timezone().ymd_opt(this_year, nd.unit, 1).single()?; if let Some(correct) = next_last_direction(date, base.date(), nd.direct) { date = base .timezone() .ymd_opt(this_year + correct, nd.unit, 1) .single()?; } ts.to_date_time(date) } ByName::DayMonth(yd) => { let mut date = base .timezone() .ymd_opt(this_year, yd.month, yd.day) .single()?; if let Some(correct) = next_last_direction(date, base.date(), yd.direct) { date = base .timezone() .ymd_opt(this_year + correct, yd.month, yd.day) .single()?; } ts.to_date_time(date) } } } } #[derive(Debug)] pub struct AbsDate { pub year: i32, pub month: u32, pub day: u32, } impl AbsDate { pub fn to_date(self, base: DateTime) -> Option> { base.timezone() .ymd_opt(self.year, self.month, self.day) .single() } } /// A generic amount of time, in either seconds, days, or months. /// /// This way, a user can decide how they want to treat days (which do /// not always have the same number of seconds) or months (which do not always /// have the same number of days). // // Skipping a given number of time units. // The subtlety is that we treat duration as seconds until we get // to months, where we want to preserve dates. So adding a month to // '5 May' gives '5 June'. Adding a month to '30 Jan' gives 'Feb 28' or 'Feb 29' // depending on whether this is a leap year. #[derive(Debug, PartialEq)] pub enum Interval { Seconds(i32), Days(i32), Months(i32), } #[derive(Debug)] pub struct Skip { pub unit: Interval, pub skip: i32, } impl Skip { pub fn to_date_time( self, base: DateTime, ts: TimeSpec, ) -> Option> { Some(match self.unit { Interval::Seconds(secs) => { base.checked_add_signed(Duration::seconds((secs as i64) * (self.skip as i64))) .unwrap() // <--- !!!! } Interval::Days(days) => { let secs = 60 * 60 * 24 * days; let date = base .checked_add_signed(Duration::seconds((secs as i64) * (self.skip as i64))) .unwrap(); if !ts.empty() { ts.to_date_time(date.date())? } else { date } } Interval::Months(mm) => { let (y, m0, d) = (base.year(), (base.month() - 1) as i32, base.day()); let delta = mm * self.skip; // our new month number let mm = m0 + delta; // which may run over to the next year and so forth let (y, m) = if mm >= 0 { (y + mm / 12, mm % 12 + 1) } else { let pmm = 12 - mm; (y - pmm / 12, 12 - pmm % 12 + 1) }; // let chrono work out if the result makes sense let mut date = base.timezone().ymd_opt(y, m as u32, d).single(); // dud dates like Feb 30 may result, so we back off... let mut d = d; while date.is_none() { d -= 1; if d == 0 || d < 28 { // sanity check... eprintln!("fkd date"); return None; } date = base.timezone().ymd_opt(y, m as u32, d).single(); } ts.to_date_time(date.unwrap())? } }) } pub fn to_interval(self) -> Interval { use Interval::*; match self.unit { Seconds(s) => Seconds(s * self.skip), Days(d) => Days(d * self.skip), Months(m) => Months(m * self.skip), } } } #[derive(Debug)] pub enum DateSpec { Absolute(AbsDate), // Y M D (e.g. 2018-06-02, 4 July 2017) Relative(Skip), // n U (e.g. 2min, 3 years ago, -2d) FromName(ByName), // (e.g. 'next fri', 'jul') } impl DateSpec { pub fn absolute(y: u32, m: u32, d: u32) -> DateSpec { DateSpec::Absolute(AbsDate { year: y as i32, month: m, day: d, }) } pub fn from_day_month(d: u32, m: u32, direct: Direction) -> DateSpec { DateSpec::FromName(ByName::from_day_month(d, m, direct)) } pub fn skip(unit: Interval, n: i32) -> DateSpec { DateSpec::Relative(Skip { unit: unit, skip: n, }) } pub fn to_date_time( self, base: DateTime, ts: TimeSpec, american: bool, ) -> Option> where Tz::Offset: Copy, { use DateSpec::*; match self { Absolute(ad) => ts.to_date_time(ad.to_date(base)?), Relative(skip) => skip.to_date_time(base, ts), // might need time FromName(byname) => byname.to_date_time(base, ts, american), } } } #[derive(Debug)] pub struct TimeSpec { pub hour: u32, pub min: u32, pub sec: u32, pub empty: bool, pub offset: Option, pub microsec: u32, } impl TimeSpec { pub fn new(hour: u32, min: u32, sec: u32, microsec: u32) -> TimeSpec { TimeSpec { hour, min, sec, empty: false, offset: None, microsec, } } pub fn new_with_offset(hour: u32, min: u32, sec: u32, offset: i64, microsec: u32) -> TimeSpec { TimeSpec { hour, min, sec, empty: false, offset: Some(offset), microsec, } } pub fn new_empty() -> TimeSpec { TimeSpec { hour: 0, min: 0, sec: 0, empty: true, offset: None, microsec: 0, } } pub fn empty(&self) -> bool { self.empty } pub fn to_date_time(self, d: Date) -> Option> { let dt = d.and_hms_micro(self.hour, self.min, self.sec, self.microsec); if let Some(offs) = self.offset { let zoffset = dt.offset().clone(); let tstamp = dt.timestamp() - offs + zoffset.fix().local_minus_utc() as i64; let nd = NaiveDateTime::from_timestamp(tstamp, 1000 * self.microsec); Some(DateTime::from_utc(nd, zoffset)) } else { Some(dt) } } } #[derive(Debug)] pub struct DateTimeSpec { pub date: Option, pub time: Option, } // same as chrono's 'count days from monday' convention pub fn week_day(s: &str) -> Option { if s.len() < 3 { return None; } Some(match &s[0..3] { "sun" => 6, "mon" => 0, "tue" => 1, "wed" => 2, "thu" => 3, "fri" => 4, "sat" => 5, _ => return None, }) } pub fn month_name(s: &str) -> Option { if s.len() < 3 { return None; } Some(match &s[0..3] { "jan" => 1, "feb" => 2, "mar" => 3, "apr" => 4, "may" => 5, "jun" => 6, "jul" => 7, "aug" => 8, "sep" => 9, "oct" => 10, "nov" => 11, "dec" => 12, _ => return None, }) } pub fn time_unit(s: &str) -> Option { use Interval::*; let name = if s.len() < 3 { match &s[0..1] { "s" => "sec", "m" => "min", "h" => "hou", "w" => "wee", "d" => "day", "y" => "yea", _ => return None, } } else { &s[0..3] }; Some(match name { "sec" => Seconds(1), "min" => Seconds(60), "hou" => Seconds(60 * 60), "day" => Days(1), "wee" => Days(7), "mon" => Months(1), "yea" => Months(12), _ => return None, }) }