interim-0.1.2/.cargo_vcs_info.json0000644000000001360000000000100125030ustar { "git": { "sha1": "229498fe8ab218378fcfd98bb6edf0959eecb2b1" }, "path_in_vcs": "" }interim-0.1.2/.gitignore000064400000000000000000000000501046102023000132560ustar 00000000000000scratch/ /target/ **/*.rs.bk Cargo.lock interim-0.1.2/Cargo.lock0000644000000237230000000000100104650ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "beef" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" [[package]] name = "bumpalo" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "cc" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "windows-targets", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "interim" version = "0.1.2" dependencies = [ "chrono", "logos", "time", ] [[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "logos" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "161971eb88a0da7ae0c333e1063467c5b5727e7fb6b710b8db4814eade3a42e8" dependencies = [ "logos-derive", ] [[package]] name = "logos-codegen" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e31badd9de5131fdf4921f6473d457e3dd85b11b7f091ceb50e4df7c3eeb12a" dependencies = [ "beef", "fnv", "lazy_static", "proc-macro2", "quote", "regex-syntax", "syn", ] [[package]] name = "logos-derive" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c2a69b3eb68d5bd595107c9ee58d7e07fe2bb5e360cc85b0f084dedac80de0a" dependencies = [ "logos-codegen", ] [[package]] name = "num-traits" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] [[package]] name = "num_threads" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" dependencies = [ "libc", ] [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "proc-macro2" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "regex-syntax" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "syn" version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "time" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" dependencies = [ "itoa", "libc", "num_threads", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "wasm-bindgen" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] name = "windows_i686_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" interim-0.1.2/Cargo.toml0000644000000025370000000000100105100ustar # 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] edition = "2021" rust-version = "1.65.0" name = "interim" version = "0.1.2" authors = ["Conrad Ludgate >().join(" "); #[cfg(feature = "time")] { use interim::{parse_date_string, Dialect}; use time::OffsetDateTime; println!( "{}", parse_date_string(arg.as_str(), OffsetDateTime::now_utc(), Dialect::Us).unwrap() ); } #[cfg(feature = "chrono")] { use chrono::Local; use interim::{parse_date_string, Dialect}; println!( "{}", parse_date_string(arg.as_str(), Local::now(), Dialect::Us).unwrap() ); } #[cfg(not(any(feature = "time", feature = "chrono")))] { eprintln!("Please enable either time or chrono feature") } } interim-0.1.2/readme.md000064400000000000000000000075001046102023000130540ustar 00000000000000# interim interim started as a fork, but ended up being a complete over-haul of [chrono-english](https://github.com/stevedonovan/chrono-english). The API surface is the same, although there's some key differences ## Improvements Why use interim over chrono-english? 1. chrono-english is not actively maintained: https://github.com/stevedonovan/chrono-english/issues/22 2. interim simplifies a lot of the code, removing a lot of potential panics and adds some optimisations. 3. supports `no_std`, as well as the `time` crate ## Features - `std`: This crate is `no_std` compatible. Disable the default-features to disable the std-lib features (just error reporting) - `time`: This crate is compatible with the [time crate](https://github.com/time-rs/time). - `chrono`: This crate is compatible with the [chrono crate](https://github.com/chronotope/chrono). ## Supported Formats `interim` 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 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. ```rust use interim::{parse_date_string, Dialect}; use chrono::Local; 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. ```rust use interim::{parse_duration, Interval}; assert_eq!(parse_duration("15m ago").unwrap(), Interval::Seconds(-15 * 60)); ``` You can test out the library by using the CLI example, ```bash cargo run --example cli --features time 'next day' ``` interim-0.1.2/src/datetime.rs000064400000000000000000000134361046102023000142330ustar 00000000000000pub trait Date: Clone + PartialOrd { fn from_ymd(year: i32, month: u8, day: u8) -> Option; fn offset_months(self, months: i32) -> Option; fn offset_days(self, days: i64) -> Option; fn year(&self) -> i32; fn weekday(&self) -> u8; } pub trait Time: Clone + PartialOrd { fn from_hms(h: u32, m: u32, s: u32) -> Option; fn with_micros(self, ms: u32) -> Option; } pub trait DateTime: Sized { type TimeZone: Timezone; type Date: Date; type Time: Time; fn new(tz: Self::TimeZone, date: Self::Date, time: Self::Time) -> Self; fn split(self) -> (Self::TimeZone, Self::Date, Self::Time); fn offset_seconds(self, secs: i64) -> Option; } pub trait Timezone { fn local_minus_utc(&self) -> i64; } #[cfg(feature = "chrono")] mod chrono { use chrono::{Duration, NaiveDate, NaiveTime, Offset, TimeZone, Timelike}; use super::{Date, DateTime, Time, Timezone}; #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] impl Date for NaiveDate { fn from_ymd(year: i32, month: u8, day: u8) -> Option { NaiveDate::from_ymd_opt(year, month as u32, day as u32) } fn offset_months(self, months: i32) -> Option { if months >= 0 { self.checked_add_months(chrono::Months::new(months as u32)) } else { self.checked_sub_months(chrono::Months::new(-months as u32)) } } fn offset_days(self, days: i64) -> Option { self.checked_add_signed(Duration::days(days)) } fn year(&self) -> i32 { chrono::Datelike::year(self) } fn weekday(&self) -> u8 { chrono::Datelike::weekday(self).num_days_from_monday() as u8 } } #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] impl Time for NaiveTime { fn from_hms(h: u32, m: u32, s: u32) -> Option { NaiveTime::from_hms_opt(h, m, s) } fn with_micros(self, ms: u32) -> Option { self.with_nanosecond(ms.checked_mul(1_000)?) } } #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] impl DateTime for chrono::DateTime where Tz::Offset: Timezone, { type TimeZone = Tz::Offset; type Date = NaiveDate; type Time = NaiveTime; fn new(tz: Self::TimeZone, date: Self::Date, time: Self::Time) -> Self { Self::from_naive_utc_and_offset(date.and_time(time) - tz.fix(), tz) } fn split(self) -> (Self::TimeZone, Self::Date, Self::Time) { (self.offset().clone(), self.date_naive(), self.time()) } fn offset_seconds(self, secs: i64) -> Option { self.checked_add_signed(Duration::seconds(secs)) } } #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] impl Timezone for chrono::FixedOffset { fn local_minus_utc(&self) -> i64 { self.local_minus_utc() as i64 } } #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] impl Timezone for chrono::Utc { fn local_minus_utc(&self) -> i64 { 0 } } } #[cfg(feature = "time")] mod time { use super::{Date, DateTime, Time, Timezone}; #[cfg_attr(docsrs, doc(cfg(feature = "time")))] impl Date for time::Date { fn from_ymd(year: i32, month: u8, day: u8) -> Option { time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day).ok() } fn offset_months(self, months: i32) -> Option { // need to calculate this manually :( let (mut y, mut m, d) = self.to_calendar_date(); y += months / 12; let mut months = months % 12; if months < 0 { months += 12; y -= 1; } // months will be between 0..12 let mut m1 = m as u8 + months as u8; if m1 > 12 { m1 -= 12; y += 1; } m = time::Month::try_from(m1 as u8).ok()?; let max_day = time::util::days_in_year_month(y, m); let d = d.min(max_day); time::Date::from_calendar_date(y, m, d).ok() } fn offset_days(self, days: i64) -> Option { self.checked_add(time::Duration::days(days)) } fn year(&self) -> i32 { time::Date::year(*self) } fn weekday(&self) -> u8 { time::Date::weekday(*self).number_days_from_monday() } } #[cfg_attr(docsrs, doc(cfg(feature = "time")))] impl Time for time::Time { fn from_hms(h: u32, m: u32, s: u32) -> Option { time::Time::from_hms( u8::try_from(h).ok()?, u8::try_from(m).ok()?, u8::try_from(s).ok()?, ) .ok() } fn with_micros(self, ms: u32) -> Option { self.replace_microsecond(ms).ok() } } #[cfg_attr(docsrs, doc(cfg(feature = "time")))] impl DateTime for time::OffsetDateTime { type TimeZone = time::UtcOffset; type Date = time::Date; type Time = time::Time; fn new(tz: Self::TimeZone, date: Self::Date, time: Self::Time) -> Self { time::PrimitiveDateTime::new(date, time).assume_offset(tz) } fn split(self) -> (Self::TimeZone, Self::Date, Self::Time) { (self.offset(), self.date(), self.time()) } fn offset_seconds(self, secs: i64) -> Option { self.checked_add(time::Duration::seconds(secs)) } } #[cfg_attr(docsrs, doc(cfg(feature = "time")))] impl Timezone for time::UtcOffset { fn local_minus_utc(&self) -> i64 { self.whole_seconds() as i64 } } } interim-0.1.2/src/errors.rs000064400000000000000000000030051046102023000137420ustar 00000000000000// use core::error::Error; use logos::Span; #[derive(Debug, PartialEq, Eq, Clone)] /// Error types for parsing and processing date/time inputs pub enum DateError { ExpectedToken(&'static str, Span), EndOfText(&'static str), MissingDate, MissingTime, UnexpectedDate, UnexpectedAbsoluteDate, UnexpectedTime, } #[cfg(feature = "std")] mod std { use super::DateError; use std::fmt; impl fmt::Display for DateError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { DateError::ExpectedToken(message, span) => { write!(f, "expected {message} as position {span:?}") } DateError::EndOfText(message) => { write!(f, "expected {message} at the end of the input") } DateError::MissingDate => f.write_str("date could not be parsed from input"), DateError::MissingTime => f.write_str("time could not be parsed from input"), DateError::UnexpectedDate => { f.write_str("expected relative date, found a named date") } DateError::UnexpectedAbsoluteDate => { f.write_str("expected relative date, found an exact date") } DateError::UnexpectedTime => f.write_str("expected duration, found time"), } } } impl std::error::Error for DateError {} } pub type DateResult = Result; interim-0.1.2/src/lib.rs000064400000000000000000000360551046102023000132070ustar 00000000000000//! # interim //! //! interim started as a fork, but ended up being a complete over-haul of [chrono-english](https://github.com/stevedonovan/chrono-english). //! //! The API surface is the same, and all the original tests from chrono-english still pass, although there's some key differences //! //! ## Improvements //! //! Why use interim over chrono-english? //! //! 1. chrono-english is not actively maintained: //! 2. interim simplifies a lot of the code, removing a lot of potential panics and adds some optimisations. //! 3. supports `no_std`, as well as the `time` crate //! //! ## Features //! //! * `std`: This crate is `no_std` compatible. Disable the default-features to disable the std-lib features (just error reporting) //! * `time`: This crate is compatible with the [time crate](https://github.com/time-rs/time). //! * `chrono`: This crate is compatible with the [chrono crate](https://github.com/chronotope/chrono). //! //! ## 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 //! use interim::{parse_date_string, Dialect}; //! use chrono::Local; //! //! 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 interim::{parse_duration, Interval}; //! //! assert_eq!(parse_duration("15m ago").unwrap(), Interval::Seconds(-15 * 60)); //! ``` #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(not(feature = "std"), no_std)] #![warn(clippy::pedantic)] #![allow( clippy::if_not_else, clippy::missing_errors_doc, clippy::module_name_repetitions, clippy::too_many_lines, clippy::cast_lossless, clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss )] /// A collection of traits to abstract over date-time implementations pub mod datetime; mod errors; mod parser; mod types; use datetime::DateTime; pub use errors::{DateError, DateResult}; pub use types::Interval; use types::{DateSpec, DateTimeSpec}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Form of english dates to parse pub enum Dialect { Uk, Us, } /// Parse a date-time from the text, potentially relative to `now`. Accepts /// a [`Dialect`] to support some slightly different text parsing behaviour. /// /// ``` /// use interim::{parse_date_string, Dialect}; /// use chrono::{Utc, TimeZone}; /// /// let now = Utc.with_ymd_and_hms(2022, 9, 17, 13, 27, 0).unwrap(); /// let this_friday = parse_date_string("friday 8pm", now, Dialect::Uk).unwrap(); /// /// assert_eq!(this_friday, Utc.with_ymd_and_hms(2022, 9, 23, 20, 0, 0).unwrap()); /// ``` pub fn parse_date_string(s: &str, now: Dt, dialect: Dialect) -> DateResult
{ into_date_string(parser::DateParser::new(s).parse(dialect)?, now, dialect) } fn into_date_string(d: DateTimeSpec, now: Dt, dialect: Dialect) -> DateResult
{ // we may have explicit hour:minute:sec if let Some(dspec) = d.date { dspec .into_date_time(now, d.time, dialect) .ok_or(DateError::MissingDate) } else if let Some(tspec) = d.time { let (tz, date, _) = now.split(); // no date, use todays date tspec.into_date_time(tz, date).ok_or(DateError::MissingTime) } else { Err(DateError::MissingTime) } } /// Parse an [`Interval`] from the text /// /// ``` /// use interim::{parse_duration, Interval}; /// use chrono::{Utc, TimeZone}; /// /// let now = Utc.with_ymd_and_hms(2022, 9, 17, 13, 27, 0).unwrap(); /// let week_ago = parse_duration("1 week ago").unwrap(); /// let minutes = parse_duration("10m").unwrap(); /// /// assert_eq!(week_ago, Interval::Days(-7)); /// assert_eq!(minutes, Interval::Seconds(10*60)); /// ``` pub fn parse_duration(s: &str) -> DateResult { let d = parser::DateParser::new(s).parse(Dialect::Uk)?; if d.time.is_some() { return Err(DateError::UnexpectedTime); } match d.date { Some(DateSpec::Relative(skip)) => Ok(skip), Some(DateSpec::Absolute(_)) => Err(DateError::UnexpectedAbsoluteDate), Some(DateSpec::FromName(..)) => Err(DateError::UnexpectedDate), None => Err(DateError::MissingDate), } } #[cfg(test)] mod tests { use crate::{parse_duration, DateError, Dialect, Interval}; #[cfg(feature = "chrono")] #[track_caller] fn format_chrono(d: &crate::types::DateTimeSpec, dialect: Dialect) -> String { use chrono::{FixedOffset, TimeZone}; let base = FixedOffset::east_opt(7200) .unwrap() .with_ymd_and_hms(2018, 3, 21, 11, 00, 00) .unwrap(); match crate::into_date_string(d.clone(), base, dialect) { Err(e) => { panic!("unexpected error attempting to format [chrono] {d:?}\n\t{e:?}") } Ok(date) => date.format("%+").to_string(), } } #[cfg(feature = "time")] #[track_caller] fn format_time(d: &crate::types::DateTimeSpec, dialect: Dialect) -> String { use time::{Date, Month, PrimitiveDateTime, Time, UtcOffset}; let base = PrimitiveDateTime::new( Date::from_calendar_date(2018, Month::March, 21).unwrap(), Time::from_hms(11, 00, 00).unwrap(), ) .assume_offset(UtcOffset::from_whole_seconds(7200).unwrap()); match crate::into_date_string(d.clone(), base, dialect) { Err(e) => { panic!("unexpected error attempting to format [time] {d:?}\n\t{e:?}") } Ok(date) => { let format = time::format_description::parse( "[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]", ).unwrap(); date.format(&format).unwrap() } } } macro_rules! assert_date_string { ($s:literal, $dialect:ident, $expect:literal) => { let dialect = Dialect::$dialect; let input = $s; let _date = match crate::parser::DateParser::new(input).parse(dialect) { Err(e) => { panic!("unexpected error attempting to parse [chrono] {input:?}\n\t{e:?}") } Ok(date) => date, }; #[cfg(feature = "chrono")] { let output = format_chrono(&_date, dialect); let expected: &str = $expect; if output != expected { panic!("unexpected output attempting to format [chrono] {input:?}.\nexpected: {expected:?}\n parsed: {_date:?}"); } } #[cfg(feature = "time")] { let output = format_time(&_date, dialect); let expected: &str = $expect; if output != expected { panic!("unexpected output attempting to format [time] {input:?}.\nexpected: {expected:?}\n parsed: {_date:?}"); } } }; } #[test] fn basics() { // Day of week - relative to today. May have a time part assert_date_string!("friday", Uk, "2018-03-23T00:00:00+02:00"); assert_date_string!("friday 10:30", Uk, "2018-03-23T10:30:00+02:00"); assert_date_string!("friday 8pm", Uk, "2018-03-23T20:00:00+02:00"); assert_date_string!("12am", Uk, "2018-03-21T00:00:00+02:00"); assert_date_string!("12pm", Uk, "2018-03-21T12:00:00+02:00"); // The day of week is the _next_ day after today, so "Tuesday" is the next Tuesday after Wednesday assert_date_string!("tues", Uk, "2018-03-27T00:00:00+02: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_date_string!("next mon", Us, "2018-03-26T00:00:00+02:00"); // but otherwise it means the day in the next week.. assert_date_string!("next mon", Uk, "2018-04-02T00:00:00+02:00"); assert_date_string!("last fri 9.30", Uk, "2018-03-16T09:30:00+02:00"); // date expressed as month, day - relative to today. May have a time part assert_date_string!("8/11", Us, "2018-08-11T00:00:00+02:00"); assert_date_string!("last 8/11", Us, "2017-08-11T00:00:00+02:00"); assert_date_string!("last 8/11 9am", Us, "2017-08-11T09:00:00+02:00"); assert_date_string!("8/11", Uk, "2018-11-08T00:00:00+02:00"); assert_date_string!("last 8/11", Uk, "2017-11-08T00:00:00+02:00"); assert_date_string!("last 8/11 9am", Uk, "2017-11-08T09:00:00+02:00"); assert_date_string!("April 1 8.30pm", Uk, "2018-04-01T20:30:00+02:00"); // advance by time unit from today // without explicit time, use base time - otherwise override assert_date_string!("2d", Uk, "2018-03-23T11:00:00+02:00"); assert_date_string!("2d 03:00", Uk, "2018-03-23T03:00:00+02:00"); assert_date_string!("3 weeks", Uk, "2018-04-11T11:00:00+02:00"); assert_date_string!("3h", Uk, "2018-03-21T14:00:00+02:00"); assert_date_string!("6 months", Uk, "2018-09-21T00:00:00+02:00"); assert_date_string!("6 months ago", Uk, "2017-09-21T00:00:00+02:00"); assert_date_string!("3 hours ago", Uk, "2018-03-21T08:00:00+02:00"); assert_date_string!(" -3h", Uk, "2018-03-21T08:00:00+02:00"); assert_date_string!(" -3 month", Uk, "2017-12-21T00:00:00+02:00"); // absolute date with year, month, day - formal ISO and informal UK or US assert_date_string!("2017-06-30", Uk, "2017-06-30T00:00:00+02:00"); assert_date_string!("30/06/17", Uk, "2017-06-30T00:00:00+02:00"); assert_date_string!("06/30/17", Us, "2017-06-30T00:00:00+02:00"); // may be followed by time part, formal and informal assert_date_string!("2017-06-30 08:20:30", Uk, "2017-06-30T08:20:30+02:00"); assert_date_string!( "2017-06-30 08:20:30 +04:00", Uk, "2017-06-30T06:20:30+02:00" ); assert_date_string!("2017-06-30 08:20:30 +0400", Uk, "2017-06-30T06:20:30+02:00"); assert_date_string!("2017-06-30T08:20:30Z", Uk, "2017-06-30T10:20:30+02:00"); assert_date_string!("2017-06-30T08:20:30", Uk, "2017-06-30T08:20:30+02:00"); assert_date_string!("2017-06-30 12.20", Uk, "2017-06-30T12:20:00+02:00"); assert_date_string!("2017-06-30 8.20", Uk, "2017-06-30T08:20:00+02:00"); assert_date_string!("2017-06-30 12.15am", Uk, "2017-06-30T00:15:00+02:00"); assert_date_string!("2017-06-30 12.25pm", Uk, "2017-06-30T12:25:00+02:00"); assert_date_string!("2017-06-30 12:30pm", Uk, "2017-06-30T12:30:00+02:00"); assert_date_string!("2017-06-30 8.30pm", Uk, "2017-06-30T20:30:00+02:00"); assert_date_string!("2017-06-30 8:30pm", Uk, "2017-06-30T20:30:00+02:00"); assert_date_string!("2017-06-30 2am", Uk, "2017-06-30T02:00:00+02:00"); assert_date_string!("30 June 2018", Uk, "2018-06-30T00:00:00+02:00"); assert_date_string!("June 30, 2018", Uk, "2018-06-30T00:00:00+02:00"); assert_date_string!("June 30, 2018", Uk, "2018-06-30T00:00:00+02:00"); } #[test] fn durations() { macro_rules! assert_duration { ($s:literal, $expect:expr) => { let dur = parse_duration($s).unwrap(); assert_eq!(dur, $expect); }; } macro_rules! assert_duration_err { ($s:literal, $expect:expr) => { let err = parse_duration($s).unwrap_err(); assert_eq!(err, $expect); }; } assert_duration!("6h", Interval::Seconds(6 * 3600)); assert_duration!("4 hours ago", Interval::Seconds(-4 * 3600)); assert_duration!("5 min", Interval::Seconds(5 * 60)); assert_duration!("10m", Interval::Seconds(10 * 60)); assert_duration!("15m ago", Interval::Seconds(-15 * 60)); assert_duration!("1 day", Interval::Days(1)); assert_duration!("2 days ago", Interval::Days(-2)); assert_duration!("3 weeks", Interval::Days(21)); assert_duration!("2 weeks ago", Interval::Days(-14)); assert_duration!("1 month", Interval::Months(1)); assert_duration!("6 months", Interval::Months(6)); assert_duration!("8 years", Interval::Months(12 * 8)); // errors assert_duration_err!("2020-01-01", DateError::UnexpectedAbsoluteDate); assert_duration_err!("2 days 15:00", DateError::UnexpectedTime); assert_duration_err!("tuesday", DateError::UnexpectedDate); assert_duration_err!( "bananas", DateError::ExpectedToken("week day or month name", 0..7) ); } } interim-0.1.2/src/parser.rs000064400000000000000000000432001046102023000137230ustar 00000000000000use logos::{Lexer, Logos}; use crate::{ types::{ month_name, time_unit, week_day, AbsDate, ByName, DateSpec, DateTimeSpec, Direction, TimeSpec, }, DateError, DateResult, Dialect, Interval, }; // when we parse dates, there's often a bit of time parsed.. #[derive(Clone, Copy, Debug)] enum TimeKind { Formal, Informal, Am, Pm, Unknown, } pub struct DateParser<'a> { s: Lexer<'a, Tokens>, maybe_time: Option<(u32, TimeKind)>, } #[derive(logos::Logos, Debug, PartialEq, Eq, Clone, Copy)] #[logos(skip r"[ \t\n\f]+")] enum Tokens { #[regex("[0-9]{1,4}", |lex| lex.slice().parse().map_err(|_| ()))] Number(u32), #[regex("[a-zA-Z]+")] Ident, // punctuation #[token("-")] Dash, #[token("/")] Slash, #[token(":")] Colon, #[token(".")] Dot, #[token(",")] Comma, #[token("+")] Plus, } impl<'a> DateParser<'a> { pub fn new(text: &'a str) -> DateParser<'a> { DateParser { s: Tokens::lexer(text), maybe_time: None, } } fn next_num(&mut self) -> DateResult { match self.s.next() { Some(Ok(Tokens::Number(n))) => Ok(n), Some(_) => Err(DateError::ExpectedToken("number", self.s.span())), None => Err(DateError::EndOfText("number")), } } fn iso_date(&mut self, year: i32) -> DateResult { let month = self.next_num()?; match self.s.next() { Some(Ok(Tokens::Dash)) => {} Some(_) => return Err(DateError::ExpectedToken("'-'", self.s.span())), None => return Err(DateError::EndOfText("'-'")), } let day = self.next_num()?; Ok(DateSpec::Absolute(AbsDate { year, month, day })) } // We have already parsed maybe the next/last/... // and the first set of numbers followed by the slash // // US: // mm/dd/yy // mm/dd/yyyy // next mm/dd // // UK: // dd/mm/yy // dd/mm/yyyy // next dd/mm fn informal_date( &mut self, day_or_month: u32, dialect: Dialect, direct: Direction, ) -> DateResult { let month_or_day = self.next_num()?; let (day, month) = if dialect == Dialect::Us { (month_or_day, day_or_month) } else { (day_or_month, month_or_day) }; let s = self.s.clone(); if self.s.next() != Some(Ok(Tokens::Slash)) { // backtrack self.s = s; Ok(DateSpec::FromName(ByName::DayMonth { day, month }, direct)) } else { // pivot (1940, 2040) let year = match self.next_num()? as i32 { y @ 0..=40 => 2000 + y, y @ 41..=99 => 1900 + y, y => y, }; Ok(DateSpec::Absolute(AbsDate { year, month, day })) } } fn parse_date(&mut self, dialect: Dialect) -> DateResult> { let (sign, direct); let token = match self.s.next() { Some(Ok(Tokens::Dash)) => { sign = true; direct = None; self.s.next() } Some(Ok(Tokens::Ident)) => { sign = false; direct = match self.s.slice() { "now" | "today" => return Ok(Some(DateSpec::Relative(Interval::Days(0)))), "yesterday" => return Ok(Some(DateSpec::Relative(Interval::Days(-1)))), "tomorrow" => return Ok(Some(DateSpec::Relative(Interval::Days(1)))), "next" => Some(Direction::Next), "last" => Some(Direction::Last), "this" => Some(Direction::Here), _ => None, }; if direct.is_some() { // consume self.s.next() } else { Some(Ok(Tokens::Ident)) } } t => { sign = false; direct = None; t } }; match token { // date needs some token None => Err(DateError::EndOfText("empty date string")), // none of these characters begin a date or duration Some( Ok( Tokens::Colon | Tokens::Comma | Tokens::Dash | Tokens::Dot | Tokens::Slash | Tokens::Plus, ) | Err(()), ) => Err(DateError::MissingDate), // '-June' doesn't make sense Some(Ok(Tokens::Ident)) if sign => { Err(DateError::ExpectedToken("number", self.s.span())) } // {weekday} [{time}] // {month} [{day}, {year}] [{time}] // {month} [{day}] [{time}] Some(Ok(Tokens::Ident)) => { let direct = direct.unwrap_or(Direction::Here); if let Some(month) = month_name(self.s.slice()) { // {month} [{day}, {year}] // {month} [{day}] [{time}] if let Some(Ok(Tokens::Number(day))) = self.s.next() { let s = self.s.clone(); if self.s.next() == Some(Ok(Tokens::Comma)) { // comma found, expect year let year = self.next_num()? as i32; Ok(Some(DateSpec::Absolute(AbsDate { year, month, day }))) } else { // no comma found, we might expect a time component (if any) // backtrack, we'll try parse the time component later self.s = s; Ok(Some(DateSpec::FromName( ByName::DayMonth { day, month }, direct, ))) } } else { // We only have a month name to work with Ok(Some(DateSpec::FromName(ByName::MonthName(month), direct))) } } else if let Some(weekday) = week_day(self.s.slice()) { // {weekday} [{time}] // we'll try parse the time component later Ok(Some(DateSpec::FromName(ByName::WeekDay(weekday), direct))) } else { Err(DateError::ExpectedToken( "week day or month name", self.s.span(), )) } } // {day}/{month} // {month}/{day} // {day} {month} // {n} {interval} // {year}-{month}-{day} Some(Ok(Tokens::Number(n))) => { match self.s.next() { // if sign is set, we should expect something like '- 5 minutes' None if sign => Err(DateError::EndOfText("duration")), // we want a full date Some(Ok(Tokens::Comma | Tokens::Plus | Tokens::Number(_)) | Err(())) => { Err(DateError::ExpectedToken("date", self.s.span())) } // if direct is set, we should expect a day or month to direct against None | Some(Ok(Tokens::Colon | Tokens::Dot | Tokens::Dash)) if direct.is_some() => { Err(DateError::EndOfText("day or month name")) } // if no extra tokens, this is probably just a year None => Ok(Some(DateSpec::Absolute(AbsDate { year: n as i32, month: 1, day: 1, }))), Some(Ok(Tokens::Ident)) => { let direct = direct.unwrap_or(Direction::Here); let name = self.s.slice(); if let Some(month) = month_name(name) { let day = n; if let Some(Ok(Tokens::Number(year))) = self.s.next() { // 4 July 2017 let year = year as i32; Ok(Some(DateSpec::Absolute(AbsDate { year, month, day }))) } else { // 4 July Ok(Some(DateSpec::FromName( ByName::DayMonth { day, month }, direct, ))) } } else if let Some(u) = time_unit(name) { let n = n as i32; // '2 days' if sign { Ok(Some(DateSpec::Relative(u * -n))) } else { match self.s.next() { Some(Ok(Tokens::Ident)) if self.s.slice() == "ago" => { Ok(Some(DateSpec::Relative(u * -n))) } Some(Ok(Tokens::Ident)) => { Err(DateError::ExpectedToken("'ago'", self.s.span())) } Some(Ok(Tokens::Number(h))) => { self.maybe_time = Some((h, TimeKind::Unknown)); Ok(Some(DateSpec::Relative(u * n))) } _ => Ok(Some(DateSpec::Relative(u * n))), } } } else if name == "am" { self.maybe_time = Some((n, TimeKind::Am)); Ok(None) } else if name == "pm" { self.maybe_time = Some((n, TimeKind::Pm)); Ok(None) } else { Err(DateError::ExpectedToken( "month or time unit", self.s.span(), )) } } Some(Ok(Tokens::Colon)) => { self.maybe_time = Some((n, TimeKind::Formal)); Ok(None) } Some(Ok(Tokens::Dot)) => { self.maybe_time = Some((n, TimeKind::Informal)); Ok(None) } Some(Ok(Tokens::Dash)) => Ok(Some(self.iso_date(n as i32)?)), Some(Ok(Tokens::Slash)) => Ok(Some(self.informal_date( n, dialect, direct.unwrap_or(Direction::Here), )?)), } } } } fn formal_time(&mut self, hour: u32) -> DateResult { let min = self.next_num()?; let mut sec = 0; let mut micros = 0; // minute may be followed by [:secs][am|pm] let tnext = match self.s.next() { Some(Ok(Tokens::Colon)) => { sec = self.next_num()?; match self.s.next() { Some(Ok(Tokens::Dot)) => { // after a `.` implies these are subseconds. // We only care for microsecond precision, so let's // get only the 6 most significant digits micros = self.next_num()?; while micros > 1_000_000 { micros /= 10; } self.s.next() } t => t, } } // we don't expect any of these after parsing minutes Some( Ok(Tokens::Dash | Tokens::Slash | Tokens::Dot | Tokens::Comma | Tokens::Plus) | Err(()), ) => { return Err(DateError::ExpectedToken("':'", self.s.span())); } t => t, }; match tnext { // we need no timezone or hour offset. All good :) None => Ok(TimeSpec::new(hour, min, sec, micros)), // +/- timezone offset Some(Ok(tok @ (Tokens::Plus | Tokens::Dash))) => { let sign = if tok == Tokens::Dash { -1 } else { 1 }; // after a +/-, we expect a numerical offset. // either HH:MM or HHMM let mut hours = self.next_num()?; let s = self.s.clone(); let minutes = if self.s.next() != Some(Ok(Tokens::Colon)) { // backtrack, we should have the hours and minutes in the single number self.s = s; // 0030 // ^^ let minutes = hours % 100; hours /= 100; minutes } else { // 02:00 // ^^ self.next_num()? }; // hours and minutes offset in seconds let res = 60 * (minutes + 60 * hours); let offset = i64::from(res) * sign; Ok(TimeSpec::new(hour, min, sec, micros).with_offset(offset)) } Some(Ok(Tokens::Ident)) => match self.s.slice() { // 0-offset timezone "Z" => Ok(TimeSpec::new(hour, min, sec, micros).with_offset(0)), // morning "am" if hour == 12 => Ok(TimeSpec::new(0, min, sec, micros)), "am" => Ok(TimeSpec::new(hour, min, sec, micros)), // afternoon "pm" if hour == 12 => Ok(TimeSpec::new(12, min, sec, micros)), "pm" => Ok(TimeSpec::new(hour + 12, min, sec, micros)), _ => Err(DateError::ExpectedToken("expected Z/am/pm", self.s.span())), }, Some( Ok(Tokens::Slash | Tokens::Colon | Tokens::Dot | Tokens::Comma | Tokens::Number(_)) | Err(()), ) => Err(DateError::ExpectedToken("expected timezone", self.s.span())), } } fn informal_time(&mut self, hour: u32) -> DateResult { let min = self.next_num()?; let hour = match self.s.next() { None => hour, Some(Ok(Tokens::Ident)) if self.s.slice() == "am" && hour == 12 => 0, Some(Ok(Tokens::Ident)) if self.s.slice() == "am" => hour, Some(Ok(Tokens::Ident)) if self.s.slice() == "pm" && hour == 12 => 12, Some(Ok(Tokens::Ident)) if self.s.slice() == "pm" => hour + 12, Some(_) => return Err(DateError::ExpectedToken("expected am/pm", self.s.span())), }; Ok(TimeSpec::new(hour, min, 0, 0)) } pub fn parse_time(&mut self) -> DateResult> { // here the date parser looked ahead and saw an hour followed by some separator if let Some((h, kind)) = self.maybe_time { Ok(Some(match kind { TimeKind::Formal => self.formal_time(h)?, TimeKind::Informal => self.informal_time(h)?, TimeKind::Am if h == 12 => TimeSpec::new(0, 0, 0, 0), TimeKind::Am => TimeSpec::new(h, 0, 0, 0), TimeKind::Pm if h == 12 => TimeSpec::new(12, 0, 0, 0), TimeKind::Pm => TimeSpec::new(h + 12, 0, 0, 0), TimeKind::Unknown => match self.s.next() { Some(Ok(Tokens::Colon)) => self.formal_time(h)?, Some(Ok(Tokens::Dot)) => self.informal_time(h)?, Some(_) => return Err(DateError::ExpectedToken(": or .", self.s.span())), None => return Err(DateError::EndOfText(": or .")), }, })) } else { let s = self.s.clone(); if self.s.next() != Some(Ok(Tokens::Ident)) || self.s.slice() != "T" { // backtrack if we weren't able to consume a 'T' time separator self.s = s; } // we're parsing times so we should expect an hour number. // if we don't find one, then there's no time here let hour = match self.s.next() { None => return Ok(None), Some(Ok(Tokens::Number(n))) => n, Some(_) => return Err(DateError::ExpectedToken("number", self.s.span())), }; match self.s.next() { // hh:mm Some(Ok(Tokens::Colon)) => self.formal_time(hour).map(Some), // hh.mm Some(Ok(Tokens::Dot)) => self.informal_time(hour).map(Some), // 9am Some(Ok(Tokens::Ident)) => match self.s.slice() { "am" => Ok(Some(TimeSpec::new(hour, 0, 0, 0))), "pm" => Ok(Some(TimeSpec::new(hour + 12, 0, 0, 0))), _ => Err(DateError::ExpectedToken("am/pm", self.s.span())), }, Some(_) => Err(DateError::ExpectedToken("am/pm, ':' or '.'", self.s.span())), None => Err(DateError::EndOfText("am/pm, ':' or '.'")), } } } pub fn parse(&mut self, dialect: Dialect) -> DateResult { let date = self.parse_date(dialect)?; let time = self.parse_time()?; Ok(DateTimeSpec { date, time }) } } interim-0.1.2/src/types.rs000064400000000000000000000231771046102023000136060ustar 00000000000000use core::ops::Mul; use crate::datetime::{Date, DateTime, Time, Timezone}; use crate::Dialect; // implements next/last direction in expressions like 'next friday' and 'last 4 july' #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Direction { Next, Last, Here, } // all expressions modifiable with next/last; 'fri', 'jul', '5 may'. #[derive(Debug, Clone)] pub enum ByName { WeekDay(u8), MonthName(u32), DayMonth { day: u32, month: u32 }, } // fn add_days(base: DateTime, days: i64) -> Option> { // base.checked_add_signed(Duration::days(days)) // } fn next_last_direction(date: &T, base: &T, direct: Direction) -> Option { match (date.partial_cmp(base), direct) { (Some(core::cmp::Ordering::Greater), Direction::Last) => Some(-1), (Some(core::cmp::Ordering::Less), Direction::Next) => Some(1), _ => None, } } impl ByName { pub fn into_date_time( self, base: Dt, ts: Option, dialect: Dialect, mut direct: Direction, ) -> Option
{ let (tz, base_date, base_time) = base.split(); let ts = ts.unwrap_or(TimeSpec::new(0, 0, 0, 0)); let this_year = base_date.year(); let date = match self { ByName::WeekDay(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 = false; match direct { Direction::Here => direct = Direction::Next, Direction::Next if dialect == Dialect::Uk => { extra_week = true; } _ => (), }; let this_day = base_date.weekday() as i64; let that_day = nd as i64; let diff_days = that_day - this_day; let mut date = base_date.clone().offset_days(diff_days)?; if let Some(correct) = next_last_direction(&date, &base_date, direct) { date = date.offset_days(7 * correct as i64)?; } if extra_week { date = date.offset_days(7)?; } if diff_days == 0 { // same day - comparing times will determine which way we swing... let this_time = ::from_hms(ts.hour, ts.min, ts.sec)?; if let Some(correct) = next_last_direction(&this_time, &base_time, direct) { date = date.offset_days(7 * correct as i64)?; } } date } ByName::MonthName(month) => { let mut date = ::from_ymd(this_year, month as u8, 1)?; if let Some(correct) = next_last_direction(&date, &base_date, direct) { date = ::from_ymd(this_year + correct, month as u8, 1)?; } date } ByName::DayMonth { day, month } => { let mut date = ::from_ymd(this_year, month as u8, day as u8)?; if let Some(correct) = next_last_direction(&date, &base_date, direct) { date = ::from_ymd(this_year + correct, month as u8, day as u8)?; } date } }; ts.into_date_time(tz, date) } } #[derive(Debug, Clone)] pub struct AbsDate { pub year: i32, pub month: u32, pub day: u32, } impl AbsDate { pub fn into_date(self) -> Option { D::from_ymd(self.year, self.month as u8, self.day as u8) } } /// 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, Eq, Clone)] pub enum Interval { Seconds(i32), Days(i32), Months(i32), } impl Mul for Interval { type Output = Interval; fn mul(self, rhs: i32) -> Self::Output { match self { Interval::Seconds(x) => Interval::Seconds(x * rhs), Interval::Days(x) => Interval::Days(x * rhs), Interval::Months(x) => Interval::Months(x * rhs), } } } impl Interval { fn into_date_time(self, base: Dt, ts: Option) -> Option
{ match self { Interval::Seconds(secs) => { // since numbers of seconds _is a timespec_, we don't add the timespec on top // eg now + 15m shouldn't then process 12pm after it. // Ideally Interval::Seconds should be part of timespec. base.offset_seconds(secs as i64) } Interval::Days(days) => { let (tz, date, time) = base.split(); let date = date.offset_days(days as i64)?; if let Some(ts) = ts { ts.into_date_time(tz, date) } else { Some(Dt::new(tz, date, time)) } } Interval::Months(months) => { let (tz, date, _) = base.split(); let date = date.offset_months(months)?; if let Some(ts) = ts { ts.into_date_time(tz, date) } else { let time = ::from_hms(0, 0, 0)?; Some(Dt::new(tz, date, time)) } } } } } #[derive(Debug, Clone)] pub enum DateSpec { Absolute(AbsDate), // Y M D (e.g. 2018-06-02, 4 July 2017) Relative(Interval), // n U (e.g. 2min, 3 years ago, -2d) FromName(ByName, Direction), // (e.g. 'next fri', 'jul') } impl DateSpec { pub fn into_date_time( self, base: Dt, ts: Option, dialect: Dialect, ) -> Option
{ match self { DateSpec::Absolute(ad) => match ts { Some(ts) => ts.into_date_time(base.split().0, ad.into_date()?), None => Some(Dt::new( base.split().0, ad.into_date::()?, ::from_hms(0, 0, 0)?, )), }, DateSpec::Relative(skip) => skip.into_date_time(base, ts), DateSpec::FromName(byname, direct) => byname.into_date_time(base, ts, dialect, direct), } } } #[derive(Debug, Clone)] pub struct TimeSpec { pub hour: u32, pub min: u32, pub sec: u32, pub microsec: u32, pub offset: Option, } impl TimeSpec { pub const fn new(hour: u32, min: u32, sec: u32, microsec: u32) -> Self { Self { hour, min, sec, microsec, offset: None, } } pub fn with_offset(mut self, offset: i64) -> Self { self.offset = Some(offset); self } pub fn into_date_time(self, tz: Dt::TimeZone, date: Dt::Date) -> Option
{ let date = date.offset_days((self.hour / 24) as i64)?; let time = ::from_hms(self.hour % 24, self.min, self.sec)? .with_micros(self.microsec)?; if let Some(offs) = self.offset { let offset = tz.local_minus_utc() - offs; Dt::new(tz, date, time).offset_seconds(offset) } else { Some(Dt::new(tz, date, time)) } } } #[derive(Debug, Clone)] 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 { let mut s = match s.as_bytes() { [a, b, c, ..] => [*a, *b, *c], _ => return None, }; s.make_ascii_lowercase(); match &s { b"sun" => Some(6), b"mon" => Some(0), b"tue" => Some(1), b"wed" => Some(2), b"thu" => Some(3), b"fri" => Some(4), b"sat" => Some(5), _ => None, } } pub fn month_name(s: &str) -> Option { let mut s = match s.as_bytes() { [a, b, c, ..] => [*a, *b, *c], _ => return None, }; s.make_ascii_lowercase(); match &s { b"jan" => Some(1), b"feb" => Some(2), b"mar" => Some(3), b"apr" => Some(4), b"may" => Some(5), b"jun" => Some(6), b"jul" => Some(7), b"aug" => Some(8), b"sep" => Some(9), b"oct" => Some(10), b"nov" => Some(11), b"dec" => Some(12), _ => None, } } pub fn time_unit(s: &str) -> Option { let s = if s.len() > 3 { &s[..3] } else { s }; match s.as_bytes() { b"sec" | b"s" => Some(Interval::Seconds(1)), b"min" | b"m" => Some(Interval::Seconds(60)), b"hou" | b"h" => Some(Interval::Seconds(60 * 60)), b"day" | b"d" => Some(Interval::Days(1)), b"wee" | b"w" => Some(Interval::Days(7)), b"mon" => Some(Interval::Months(1)), b"yea" | b"y" => Some(Interval::Months(12)), _ => None, } }