diff --git a/Cargo.toml b/Cargo.toml index 3b3aa70..af0333b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "htp" version = "0.4.2" authors = ["PicoJr "] -edition = "2018" +edition = "2021" repository = "https://github.com/PicoJr/htp" description = "human time parser" license = "MIT OR Apache-2.0" @@ -13,11 +13,21 @@ include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md", "examples"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -pest = "2.0" -pest_derive = "2.0" -thiserror = "1.0.20" -chrono = "0.4.12" +pest = "2.4" +pest_derive = "2.4" +thiserror = "1.0.37" +chrono = { version = "0.4.23", optional = true } +time = { version = "0.3.17", optional = true, features = ["parsing", "macros"] } # https://github.com/rust-lang/rust/issues/88791 [package.metadata.docs.rs] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples=examples"] + +[features] +default = ["chrono", "time"] +chrono = ["dep:chrono"] +time = ["dep:time"] + +[[example]] +name = "time_parser" +path = "examples/time_parser.rs" diff --git a/examples/time_parser.rs b/examples/time_parser.rs index 423b619..e6e165d 100644 --- a/examples/time_parser.rs +++ b/examples/time_parser.rs @@ -1,14 +1,34 @@ -use chrono::Local; use std::env; use std::error::Error; +#[cfg(feature = "chrono")] +use chrono::Utc; +#[cfg(feature = "time")] +use time::OffsetDateTime; + fn main() -> Result<(), Box> { let args: Vec = env::args().collect(); let parameters = &args[1..]; - let datetime = htp::parse(¶meters.join(" "), Local::now()); - match datetime { - Ok(datetime) => println!("{:?}", datetime), - Err(e) => println!("{}", e), + + #[cfg(feature = "time")] + { + let time_result = htp::parse(¶meters.join(" "), OffsetDateTime::now_utc()); + + match time_result { + Ok(datetime) => println!("time: {}", datetime), + Err(e) => println!("time: {}", e), + } } + + #[cfg(feature = "chrono")] + { + let chrono_result = htp::parse(¶meters.join(" "), Utc::now()); + + match chrono_result { + Ok(datetime) => println!("chrono: {}", datetime), + Err(e) => println!("chrono: {}", e), + } + } + Ok(()) } diff --git a/src/interpreter.rs b/src/interpreter.rs index 21bec2a..5f0185c 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -1,8 +1,9 @@ use crate::parser::{Modifier, Quantifier, ShortcutDay, TimeClue, AMPM, HMS}; -use chrono::{DateTime, Datelike, Duration, LocalResult, TimeZone, Utc}; +use crate::unified; +use std::time::Duration; use thiserror::Error; -#[derive(Error, Debug, PartialEq)] +#[derive(Error, Debug, PartialEq, Eq)] pub enum EvaluationError { #[error("invalid time: {hour}:{minute}:{second} {am_or_pm}")] InvalidTimeAMPM { @@ -24,7 +25,7 @@ pub enum EvaluationError { }, } -fn check_hms(hms: HMS, am_or_pm_maybe: Option) -> Result { +const fn check_hms(hms: HMS, am_or_pm_maybe: Option) -> Result { let (h, m, s) = hms; let h_am_pm = match am_or_pm_maybe { None | Some(AMPM::AM) => h, @@ -50,10 +51,14 @@ fn check_hms(hms: HMS, am_or_pm_maybe: Option) -> Result( +/// +/// # Errors +/// See [`EvaluationError`] +#[cfg(any(feature = "chrono", feature = "time"))] +pub fn evaluate( time_clue: TimeClue, - now: DateTime, -) -> Result, EvaluationError> { + now: unified::DateTime, +) -> Result { evaluate_time_clue(time_clue, now, false) } @@ -63,57 +68,73 @@ pub fn evaluate( /// * if true: times without a day will be interpreted as times during the following the day. /// e.g. 19:43 will be interpreted as tomorrow at 19:43 if current time is > 19:43. /// * if false: times without a day will be interpreted as times during current day. -pub fn evaluate_time_clue( +/// +/// # Errors +/// See [`EvaluationError`] +#[cfg(any(feature = "chrono", feature = "time"))] +#[allow(clippy::cast_possible_truncation)] // QUERY: Would it make more sense to use `u8` instead of `u32` for `HMS`, and month/day on `YMD`? +pub fn evaluate_time_clue( time_clue: TimeClue, - now: DateTime, + now: unified::DateTime, assume_next_day: bool, // assume next day if only time is supplied and time < now -) -> Result, EvaluationError> { +) -> Result { match time_clue { TimeClue::Now => Ok(now), TimeClue::Time((h, m, s), am_or_pm_maybe) => { let (h, m, s) = check_hms((h, m, s), am_or_pm_maybe)?; - let d = now.date().and_hms(h, m, s); + let d = now.and_hms(h as u8, m as u8, s as u8); if assume_next_day && d < now { - Ok(d + Duration::days(1)) + Ok(d + Duration::from_secs(24 * 60 * 60)) } else { Ok(d) } } TimeClue::Relative(n, quantifier) => match quantifier { - Quantifier::Min => Ok(now - Duration::minutes(n as i64)), - Quantifier::Hours => Ok(now - Duration::hours(n as i64)), - Quantifier::Days => Ok(now - Duration::days(n as i64)), - Quantifier::Weeks => Ok(now - Duration::weeks(n as i64)), - Quantifier::Months => Ok(now - Duration::days(30 * n as i64)), // assume 1 month = 30 days + Quantifier::Min => Ok(now - Duration::from_secs(n as u64 * 60)), + Quantifier::Hours => Ok(now - Duration::from_secs(n as u64 * 60 * 60)), + Quantifier::Days => Ok(now - Duration::from_secs(n as u64 * 24 * 60 * 60)), + Quantifier::Weeks => Ok(now - Duration::from_secs(n as u64 * 7 * 24 * 60 * 60)), + Quantifier::Months => Ok(now - Duration::from_secs(30 * (n as u64 * 7 * 24 * 60 * 60))), // assume 1 month = 30 days }, TimeClue::RelativeFuture(n, quantifier) => match quantifier { - Quantifier::Min => Ok(now + Duration::minutes(n as i64)), - Quantifier::Hours => Ok(now + Duration::hours(n as i64)), - Quantifier::Days => Ok(now + Duration::days(n as i64)), - Quantifier::Weeks => Ok(now + Duration::weeks(n as i64)), - Quantifier::Months => Ok(now + Duration::days(30 * n as i64)), // assume 1 month = 30 days + Quantifier::Min => Ok(now + Duration::from_secs(n as u64 * 60)), + Quantifier::Hours => Ok(now + Duration::from_secs(n as u64 * 60 * 60)), + Quantifier::Days => Ok(now + Duration::from_secs(n as u64 * 24 * 60 * 60)), + Quantifier::Weeks => Ok(now + Duration::from_secs(n as u64 * 7 * 24 * 60 * 60)), + Quantifier::Months => Ok(now + Duration::from_secs(30 * (n as u64 * 7 * 24 * 60 * 60))), // assume 1 month = 30 days }, TimeClue::RelativeDayAt(modifier, weekday, hms_maybe, am_or_pm_maybe) => { let (h, m, s) = hms_maybe.unwrap_or((0, 0, 0)); let (h, m, s) = check_hms((h, m, s), am_or_pm_maybe)?; - let monday = now.date() - Duration::days(now.weekday().num_days_from_monday() as i64); + let monday = now + - Duration::from_secs( + u64::from(now.weekday().num_days_from_monday()) * 24 * 60 * 60, + ); match modifier { Modifier::Last => { - let same_week_day = - monday + (Duration::days(weekday.num_days_from_monday() as i64)); + let same_week_day = monday + + (Duration::from_secs( + u64::from(weekday.num_days_from_monday()) * 24 * 60 * 60, + )); if weekday.num_days_from_monday() < now.weekday().num_days_from_monday() { - Ok(same_week_day.and_hms(h, m, s)) // same week + Ok(same_week_day.and_hms(h as u8, m as u8, s as u8)) // same week } else { - Ok(same_week_day.and_hms(h, m, s) - Duration::days(7)) // last week + Ok(same_week_day.and_hms(h as u8, m as u8, s as u8) + - Duration::from_secs(7 * 24 * 60 * 60)) + // last week } } Modifier::Next => { - let same_week_day = - monday + (Duration::days(weekday.num_days_from_monday() as i64)); + let same_week_day = monday + + (Duration::from_secs( + u64::from(weekday.num_days_from_monday()) * 24 * 60 * 60, + )); if weekday.num_days_from_monday() > now.weekday().num_days_from_monday() { - Ok(same_week_day.and_hms(h, m, s)) // same week + Ok(same_week_day.and_hms(h as u8, m as u8, s as u8)) // same week } else { - Ok(same_week_day.and_hms(h, m, s) + Duration::days(7)) // next week + Ok(same_week_day.and_hms(h as u8, m as u8, s as u8) + + Duration::from_secs(7 * 24 * 60 * 60)) + // next week } } } @@ -121,31 +142,33 @@ pub fn evaluate_time_clue( TimeClue::SameWeekDayAt(weekday, hms_maybe, am_or_pm_maybe) => { let (h, m, s) = hms_maybe.unwrap_or((0, 0, 0)); let (h, m, s) = check_hms((h, m, s), am_or_pm_maybe)?; - let monday = now.date() - Duration::days(now.weekday().num_days_from_monday() as i64); - Ok((monday + Duration::days(weekday.num_days_from_monday() as i64)).and_hms(h, m, s)) + let monday = now + - Duration::from_secs( + u64::from(now.weekday().num_days_from_monday()) * 24 * 60 * 60, + ); + Ok((monday + + Duration::from_secs(u64::from(weekday.num_days_from_monday()) * 24 * 60 * 60)) + .and_hms(h as u8, m as u8, s as u8)) } TimeClue::ShortcutDayAt(rday, hms_maybe, am_or_pm_maybe) => { let (h, m, s) = hms_maybe.unwrap_or((0, 0, 0)); let (h, m, s) = check_hms((h, m, s), am_or_pm_maybe)?; match rday { - ShortcutDay::Today => Ok(now.date().and_hms(h, m, s)), - ShortcutDay::Yesterday => Ok((now.date() - Duration::days(1)).and_hms(h, m, s)), - ShortcutDay::Tomorrow => Ok((now.date() + Duration::days(1)).and_hms(h, m, s)), + ShortcutDay::Today => Ok(now.and_hms(h as u8, m as u8, s as u8)), + ShortcutDay::Yesterday => Ok( + (now - Duration::from_secs(24 * 60 * 60)).and_hms(h as u8, m as u8, s as u8) + ), + ShortcutDay::Tomorrow => Ok( + (now + Duration::from_secs(24 * 60 * 60)).and_hms(h as u8, m as u8, s as u8) + ), } } TimeClue::ISO((year, month, day), (h, m, s)) => { - let utc = Utc.ymd_opt(year, month, day).and_hms_opt(h, m, s); - match utc { - LocalResult::Single(utc) => Ok(utc.with_timezone(&now.timezone())), - _ => Err(EvaluationError::ChronoISOError { - year, - month, - day, - hour: h, - minute: m, - second: s, - }), - } + let utc = now + .and_ymd(year, month as u8, day as u8) + .and_hms(h as u8, m as u8, s as u8); + + Ok(utc) } } } @@ -155,9 +178,10 @@ mod test { use crate::interpreter::{check_hms, evaluate, evaluate_time_clue}; use crate::parser::AMPM::{AM, PM}; use crate::parser::{Modifier, TimeClue}; - use chrono::offset::TimeZone; - use chrono::Utc; - use chrono::Weekday; + #[cfg(feature = "chrono")] + use chrono::{offset::TimeZone, Utc, Weekday as ChronoWeekday}; + #[cfg(feature = "time")] + use time::{macros::format_description, PrimitiveDateTime, Weekday as TimeWeekday}; #[test] fn test_check_hms() { @@ -171,17 +195,47 @@ mod test { } #[test] - fn test_next_weekday() { + #[cfg(feature = "chrono")] + fn chrono_test_next_weekday() { let now = Utc .datetime_from_str("2020-07-12T12:45:00", "%Y-%m-%dT%H:%M:%S") .unwrap(); // sunday let expected = Utc .datetime_from_str("2020-07-17T00:00:00", "%Y-%m-%dT%H:%M:%S") - .unwrap(); + .unwrap() + .into(); + + assert_eq!( + evaluate( + TimeClue::RelativeDayAt(Modifier::Next, ChronoWeekday::Fri.into(), None, None), + now.into() + ) + .unwrap(), + expected + ); + } + + #[test] + #[cfg(feature = "time")] + fn time_test_next_weekday() { + let now = PrimitiveDateTime::parse( + "2020-07-12T12:45:00Z", + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"), + ) + .expect("failed to parse origin date") + .assume_utc(); // sunday + let expected = PrimitiveDateTime::parse( + "2020-07-17T00:00:00Z", + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"), + ) + .expect("failed to parse expected date") + .assume_utc() + .into(); + assert_eq!( evaluate( - TimeClue::RelativeDayAt(Modifier::Next, Weekday::Fri, None, None), - now + TimeClue::RelativeDayAt(Modifier::Next, TimeWeekday::Friday.into(), None, None), + now.into() ) .unwrap(), expected @@ -189,24 +243,66 @@ mod test { } #[test] - fn test_assume_next_day() { + #[cfg(feature = "chrono")] + fn chrono_test_assume_next_day() { let now = Utc .datetime_from_str("2020-07-12T12:45:00", "%Y-%m-%dT%H:%M:%S") .unwrap(); // sunday let expected = Utc .datetime_from_str("2020-07-12T08:00:00", "%Y-%m-%dT%H:%M:%S") - .unwrap(); + .unwrap() + .into(); + assert_eq!( - evaluate_time_clue(TimeClue::Time((8, 0, 0), None), now.clone(), false).unwrap(), + evaluate_time_clue(TimeClue::Time((8, 0, 0), None), now.into(), false).unwrap(), expected ); let expected = Utc .datetime_from_str("2020-07-13T08:00:00", "%Y-%m-%dT%H:%M:%S") - .unwrap(); + .unwrap() + .into(); + + assert_eq!( + evaluate_time_clue(TimeClue::Time((8, 0, 0), None), now.into(), true).unwrap(), + expected + ); + } + + #[test] + #[cfg(feature = "time")] + fn time_test_assume_next_day() { + let now = PrimitiveDateTime::parse( + "2020-07-12T12:45:00", + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"), + ) + .expect("failed to parse test date") + .assume_utc(); // sunday + + let expected = PrimitiveDateTime::parse( + "2020-07-12T08:00:00", + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"), + ) + .expect("failed to parse expected date") + .assume_utc() + .into(); + + assert_eq!( + evaluate_time_clue(TimeClue::Time((8, 0, 0), None), now.into(), false).unwrap(), + expected + ); + + let expected = PrimitiveDateTime::parse( + "2020-07-13T08:00:00", + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"), + ) + .expect("failed to parse expected date") + .assume_utc() + .into(); + assert_eq!( - evaluate_time_clue(TimeClue::Time((8, 0, 0), None), now, true).unwrap(), + evaluate_time_clue(TimeClue::Time((8, 0, 0), None), now.into(), true).unwrap(), expected ); } diff --git a/src/lib.rs b/src/lib.rs index 2e73e48..8a32b24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,25 +3,45 @@ //! //! ## Example //! -//! ``` -//! use chrono::{Utc, TimeZone}; +//! ```rust //! use htp::parse; -//! let now = Utc.datetime_from_str("2020-12-24T23:45:00", "%Y-%m-%dT%H:%M:%S").unwrap(); -//! let expected = Utc.datetime_from_str("2020-12-18T19:43:00", "%Y-%m-%dT%H:%M:%S").unwrap(); -//! let datetime = parse("last friday at 19:43", now).unwrap(); -//! assert_eq!(datetime, expected); +//! +//! // using `time` +//! #[cfg(feature = "time")] +//! { +//! use time::{PrimitiveDateTime, macros::format_description}; +//! +//! let now = PrimitiveDateTime::parse("2020-12-24T23:45:00", format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]")).unwrap().assume_utc(); +//! let expected = PrimitiveDateTime::parse("2020-12-18T19:43:00", format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]")).unwrap().assume_utc().into(); +//! let datetime = parse("last friday at 19:43", now).unwrap(); +//! assert_eq!(datetime, expected); +//! } +//! +//! // using `chrono` +//! #[cfg(feature = "chrono")] +//! { +//! use chrono::{Utc, TimeZone}; +//! +//! let now = Utc.datetime_from_str("2020-12-24T23:45:00", "%Y-%m-%dT%H:%M:%S").unwrap(); +//! let expected = Utc.datetime_from_str("2020-12-18T19:43:00", "%Y-%m-%dT%H:%M:%S").unwrap().into(); +//! let datetime = parse("last friday at 19:43", now).unwrap(); +//! assert_eq!(datetime, expected); +//! } //! ``` //! -extern crate pest; -#[macro_use] -extern crate pest_derive; -use chrono::DateTime; +#![warn(clippy::pedantic, clippy::nursery)] + +#[cfg(not(any(feature = "time", feature = "chrono")))] +compile_error!("You must enable at least one of the following features: time, chrono"); + use thiserror::Error; pub mod interpreter; pub mod parser; +mod unified; + #[derive(Error, Debug)] pub enum HTPError { #[error(transparent)] @@ -33,8 +53,13 @@ pub enum HTPError { /// Same as `parse_time_clue(s, now, false)` /// /// Parse time clue from `s` given reference time `now` in timezone `Tz`. -pub fn parse(s: &str, now: DateTime) -> Result, HTPError> { - parse_time_clue(s, now, false) +/// +/// # Errors +/// +/// - If parsing fails (see [`parser::ParseError`]) +/// - If evaluation fails (see [`interpreter::EvaluationError`]) +pub fn parse(s: &str, now: impl Into) -> Result { + parse_time_clue(s, now.into(), false) } /// Parse time clue from `s` given reference time `now` in timezone `Tz`. @@ -43,12 +68,17 @@ pub fn parse(s: &str, now: DateTime) -> Result 19:43. /// * if false: times without a day will be interpreted as times during current day. -pub fn parse_time_clue( +/// +/// # Errors +/// +/// - If parsing fails (see [`parser::ParseError`]) +/// - If evaluation fails (see [`interpreter::EvaluationError`]) +pub fn parse_time_clue( s: &str, - now: DateTime, + now: impl Into, assume_next_day: bool, -) -> Result, HTPError> { +) -> Result { let time_clue = parser::parse_time_clue_from_str(s)?; - let datetime = interpreter::evaluate_time_clue(time_clue, now, assume_next_day)?; + let datetime = interpreter::evaluate_time_clue(time_clue, now.into(), assume_next_day)?; Ok(datetime) } diff --git a/src/parser.rs b/src/parser.rs index cd0c2ed..11a78ef 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,12 +1,15 @@ -use chrono::Weekday; +use crate::unified::Weekday; use pest::iterators::{Pair, Pairs}; use pest::Parser; +use pest_derive::Parser; use std::fmt; use std::fmt::Formatter; +use std::str::FromStr; use thiserror::Error; #[derive(Parser)] #[grammar = "time.pest"] +#[allow(clippy::module_name_repetitions)] // because there's no better way pub struct TimeParser; pub type YMD = (i32, u32, u32); @@ -17,7 +20,7 @@ pub enum ParseError { #[error("invalid integer")] ParseInt(#[from] std::num::ParseIntError), #[error(transparent)] - PestError(#[from] pest::error::Error), + PestError(#[from] Box>), #[error("unexpected non matching pattern")] UnexpectedNonMatchingPattern, #[error("unknown weekday: `{0}`")] @@ -32,20 +35,26 @@ pub enum ParseError { UnknownAMPM(String), } +impl From> for ParseError { + fn from(e: pest::error::Error) -> Self { + Self::PestError(Box::new(e)) + } +} + fn weekday_from(s: &str) -> Result { match s { - "monday" | "mon" => Ok(Weekday::Mon), - "tuesday" | "tue" => Ok(Weekday::Tue), - "wednesday" | "wed" => Ok(Weekday::Wed), - "thursday" | "thu" => Ok(Weekday::Thu), - "friday" | "fri" => Ok(Weekday::Fri), - "saturday" | "sat" => Ok(Weekday::Sat), - "sunday" | "sun" => Ok(Weekday::Sun), + "monday" | "mon" => Ok(Weekday::Monday), + "tuesday" | "tue" => Ok(Weekday::Tuesday), + "wednesday" | "wed" => Ok(Weekday::Wednesday), + "thursday" | "thu" => Ok(Weekday::Thursday), + "friday" | "fri" => Ok(Weekday::Friday), + "saturday" | "sat" => Ok(Weekday::Saturday), + "sunday" | "sun" => Ok(Weekday::Sunday), _ => Err(ParseError::UnknownWeekday(s.to_string())), } } -#[derive(Error, Debug, PartialEq)] +#[derive(Error, Debug, PartialEq, Eq)] pub enum AMPM { AM, PM, @@ -54,21 +63,25 @@ pub enum AMPM { impl fmt::Display for AMPM { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - AMPM::AM => write!(f, "am"), - AMPM::PM => write!(f, "pm"), + Self::AM => write!(f, "am"), + Self::PM => write!(f, "pm"), } } } -fn am_or_pm_from(s: &str) -> Result { - match s { - "am" => Ok(AMPM::AM), - "pm" => Ok(AMPM::PM), - _ => Err(ParseError::UnknownAMPM(s.to_string())), +impl FromStr for AMPM { + type Err = ParseError; + + fn from_str(value: &str) -> Result { + match value { + "am" => Ok(Self::AM), + "pm" => Ok(Self::PM), + _ => Err(ParseError::UnknownAMPM(value.to_string())), + } } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum ShortcutDay { Today, Yesterday, @@ -84,7 +97,7 @@ fn shortcut_day_from(s: &str) -> Result { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Modifier { Last, Next, @@ -98,7 +111,7 @@ fn modifier_from(s: &str) -> Result { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Quantifier { Min, Hours, @@ -118,7 +131,7 @@ fn quantifier_from(s: &str) -> Result { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum TimeClue { /// Now. Now, @@ -157,20 +170,20 @@ fn parse_time_hms(rules_and_str: &[(Rule, &str)]) -> Result { let h: u32 = h.parse()?; - let am_or_pm = am_or_pm_from(am_or_pm)?; + let am_or_pm = am_or_pm.parse()?; Ok(TimeClue::Time((h, 0, 0), Some(am_or_pm))) } [(Rule::hms, h), (Rule::hms, m), (Rule::am_or_pm, am_or_pm)] => { let h: u32 = h.parse()?; let m: u32 = m.parse()?; - let am_or_pm = am_or_pm_from(am_or_pm)?; + let am_or_pm = am_or_pm.parse()?; Ok(TimeClue::Time((h, m, 0), Some(am_or_pm))) } [(Rule::hms, h), (Rule::hms, m), (Rule::hms, s), (Rule::am_or_pm, am_or_pm)] => { let h: u32 = h.parse()?; let m: u32 = m.parse()?; let s: u32 = s.parse()?; - let am_or_pm = am_or_pm_from(am_or_pm)?; + let am_or_pm = am_or_pm.parse()?; Ok(TimeClue::Time((h, m, s), Some(am_or_pm))) } _ => Err(ParseError::UnexpectedNonMatchingPattern), @@ -268,6 +281,9 @@ fn parse_time_clue(pairs: &[Pair]) -> Result { /// /// This function is provided in case you wish to interpret time clues /// yourself. Prefer `htp::parse`. +/// +/// # Errors +/// See [`ParseError`] pub fn parse_time_clue_from_str(s: &str) -> Result { let pairs: Pairs = TimeParser::parse(Rule::time_clue, s)?; let pairs: Vec> = pairs.flatten().collect(); @@ -279,7 +295,10 @@ mod test { use crate::parser::{ parse_time_clue_from_str, Modifier, Quantifier, ShortcutDay, TimeClue, AMPM, }; - use chrono::Weekday; + #[cfg(feature = "chrono")] + use chrono::Weekday as ChronoWeekday; + #[cfg(feature = "time")] + use time::Weekday as TimeWeekday; #[test] fn test_parse_time_ok() { @@ -307,19 +326,19 @@ mod test { #[test] fn test_parse_relative_ok() { - for s in vec!["2 min ago", "2min ago", "2minago", "2 min ago"].iter() { + for s in &["2 min ago", "2min ago", "2minago", "2 min ago"] { assert_eq!( TimeClue::Relative(2, Quantifier::Min), parse_time_clue_from_str(s).unwrap() ); } - for s in vec!["2 h ago", "2 hour ago", "2 hours ago"].iter() { + for s in &["2 h ago", "2 hour ago", "2 hours ago"] { assert_eq!( TimeClue::Relative(2, Quantifier::Hours), parse_time_clue_from_str(s).unwrap() ); } - for s in vec!["2 d ago", "2 day ago", "2 days ago"].iter() { + for s in &["2 d ago", "2 day ago", "2 days ago"] { assert_eq!( TimeClue::Relative(2, Quantifier::Days), parse_time_clue_from_str(s).unwrap() @@ -329,19 +348,19 @@ mod test { #[test] fn test_parse_relative_future_ok() { - for s in vec!["in 2 min", "in 2min", "in2min", "in 2 min"].iter() { + for s in &["in 2 min", "in 2min", "in2min", "in 2 min"] { assert_eq!( TimeClue::RelativeFuture(2, Quantifier::Min), parse_time_clue_from_str(s).unwrap() ); } - for s in vec!["in 2 h", "in 2 hour", "in 2 hours"].iter() { + for s in &["in 2 h", "in 2 hour", "in 2 hours"] { assert_eq!( TimeClue::RelativeFuture(2, Quantifier::Hours), parse_time_clue_from_str(s).unwrap() ); } - for s in vec!["in 2 d", "in 2 day", "in 2 days"].iter() { + for s in &["in 2 d", "in 2 day", "in 2 days"] { assert_eq!( TimeClue::RelativeFuture(2, Quantifier::Days), parse_time_clue_from_str(s).unwrap() @@ -378,56 +397,125 @@ mod test { } #[test] - fn test_parse_relative_day_ok() { + #[cfg(feature = "chrono")] + fn chrono_test_parse_relative_day_ok() { + assert_eq!(TimeClue::Now, parse_time_clue_from_str("now").unwrap()); + assert_eq!( + TimeClue::SameWeekDayAt(ChronoWeekday::Fri.into(), Some((19, 43, 0)), None), + parse_time_clue_from_str("friday at 19:43").unwrap() + ); + assert_eq!( + TimeClue::RelativeDayAt(Modifier::Last, ChronoWeekday::Fri.into(), None, None), + parse_time_clue_from_str("last friday").unwrap() + ); + assert_eq!( + TimeClue::RelativeDayAt( + Modifier::Last, + ChronoWeekday::Fri.into(), + Some((9, 0, 0)), + None + ), + parse_time_clue_from_str("last friday at 9").unwrap() + ); + } + + #[test] + #[cfg(feature = "time")] + fn time_test_parse_relative_day_ok() { assert_eq!(TimeClue::Now, parse_time_clue_from_str("now").unwrap()); assert_eq!( - TimeClue::SameWeekDayAt(Weekday::Fri, Some((19, 43, 0)), None), + TimeClue::SameWeekDayAt(TimeWeekday::Friday.into(), Some((19, 43, 0)), None), parse_time_clue_from_str("friday at 19:43").unwrap() ); assert_eq!( - TimeClue::RelativeDayAt(Modifier::Last, Weekday::Fri, None, None), + TimeClue::RelativeDayAt(Modifier::Last, TimeWeekday::Friday.into(), None, None), parse_time_clue_from_str("last friday").unwrap() ); assert_eq!( - TimeClue::RelativeDayAt(Modifier::Last, Weekday::Fri, Some((9, 0, 0)), None), + TimeClue::RelativeDayAt( + Modifier::Last, + TimeWeekday::Friday.into(), + Some((9, 0, 0)), + None + ), parse_time_clue_from_str("last friday at 9").unwrap() ); } #[test] - fn test_parse_same_week_ok() { + #[cfg(feature = "chrono")] + fn chrono_test_parse_same_week_ok() { let weekdays = vec![ - (Weekday::Mon, "monday"), - (Weekday::Tue, "tuesday"), - (Weekday::Wed, "wednesday"), - (Weekday::Thu, "thursday"), - (Weekday::Fri, "friday"), - (Weekday::Sat, "saturday"), - (Weekday::Sun, "sunday"), + (ChronoWeekday::Mon, "monday"), + (ChronoWeekday::Tue, "tuesday"), + (ChronoWeekday::Wed, "wednesday"), + (ChronoWeekday::Thu, "thursday"), + (ChronoWeekday::Fri, "friday"), + (ChronoWeekday::Sat, "saturday"), + (ChronoWeekday::Sun, "sunday"), ]; - for (weekday, weekday_str) in weekdays.iter() { + for (weekday, weekday_str) in &weekdays { assert_eq!( - TimeClue::SameWeekDayAt(weekday.clone(), None, None), + TimeClue::SameWeekDayAt((*weekday).into(), None, None), parse_time_clue_from_str(weekday_str).unwrap() - ) + ); + } + let weekdays = vec![ + (ChronoWeekday::Mon, "mon"), + (ChronoWeekday::Tue, "tue"), + (ChronoWeekday::Wed, "wed"), + (ChronoWeekday::Thu, "thu"), + (ChronoWeekday::Fri, "fri"), + (ChronoWeekday::Sat, "sat"), + (ChronoWeekday::Sun, "sun"), + ]; + for (weekday, weekday_str) in &weekdays { + assert_eq!( + TimeClue::SameWeekDayAt((*weekday).into(), None, None), + parse_time_clue_from_str(weekday_str).unwrap() + ); + } + assert_eq!( + TimeClue::SameWeekDayAt(ChronoWeekday::Fri.into(), Some((19, 43, 0)), None), + parse_time_clue_from_str("friday at 19:43").unwrap() + ); + } + + #[test] + #[cfg(feature = "time")] + fn time_test_parse_same_week_ok() { + let weekdays = vec![ + (TimeWeekday::Monday, "monday"), + (TimeWeekday::Tuesday, "tuesday"), + (TimeWeekday::Wednesday, "wednesday"), + (TimeWeekday::Thursday, "thursday"), + (TimeWeekday::Friday, "friday"), + (TimeWeekday::Saturday, "saturday"), + (TimeWeekday::Sunday, "sunday"), + ]; + for (weekday, weekday_str) in &weekdays { + assert_eq!( + TimeClue::SameWeekDayAt((*weekday).into(), None, None), + parse_time_clue_from_str(weekday_str).unwrap() + ); } let weekdays = vec![ - (Weekday::Mon, "mon"), - (Weekday::Tue, "tue"), - (Weekday::Wed, "wed"), - (Weekday::Thu, "thu"), - (Weekday::Fri, "fri"), - (Weekday::Sat, "sat"), - (Weekday::Sun, "sun"), + (TimeWeekday::Monday, "mon"), + (TimeWeekday::Tuesday, "tue"), + (TimeWeekday::Wednesday, "wed"), + (TimeWeekday::Thursday, "thu"), + (TimeWeekday::Friday, "fri"), + (TimeWeekday::Saturday, "sat"), + (TimeWeekday::Sunday, "sun"), ]; - for (weekday, weekday_str) in weekdays.iter() { + for (weekday, weekday_str) in &weekdays { assert_eq!( - TimeClue::SameWeekDayAt(weekday.clone(), None, None), + TimeClue::SameWeekDayAt((*weekday).into(), None, None), parse_time_clue_from_str(weekday_str).unwrap() - ) + ); } assert_eq!( - TimeClue::SameWeekDayAt(Weekday::Fri, Some((19, 43, 0)), None), + TimeClue::SameWeekDayAt(TimeWeekday::Friday.into(), Some((19, 43, 0)), None), parse_time_clue_from_str("friday at 19:43").unwrap() ); } diff --git a/src/unified.rs b/src/unified.rs new file mode 100644 index 0000000..8e11ee9 --- /dev/null +++ b/src/unified.rs @@ -0,0 +1,218 @@ +use std::{ + cmp::Ordering, + fmt, + ops::{Add, Sub}, + time::Duration, +}; + +#[cfg(feature = "chrono")] +use chrono::{Datelike, TimeZone, Timelike}; +#[cfg(feature = "time")] +use time::UtcOffset; + +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub enum DateTime { + #[cfg(feature = "time")] + Time(time::OffsetDateTime), + #[cfg(feature = "chrono")] + Chrono(chrono::DateTime), +} + +impl fmt::Display for DateTime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + #[cfg(feature = "time")] + Self::Time(t) => write!(f, "{}", t), + #[cfg(feature = "chrono")] + Self::Chrono(t) => write!(f, "{}", t), + } + } +} + +impl Add for DateTime { + type Output = Self; + + fn add(self, rhs: Duration) -> Self::Output { + match self { + #[cfg(feature = "time")] + Self::Time(t) => Self::Time(t + rhs), + #[cfg(feature = "chrono")] + Self::Chrono(t) => Self::Chrono(t + chrono::Duration::from_std(rhs).unwrap()), // chrono wants to be a unique one + } + } +} + +impl Sub for DateTime { + type Output = Self; + + fn sub(self, rhs: Duration) -> Self::Output { + match self { + #[cfg(feature = "time")] + Self::Time(t) => Self::Time(t - rhs), + #[cfg(feature = "chrono")] + Self::Chrono(t) => Self::Chrono(t - chrono::Duration::from_std(rhs).unwrap()), // chrono wants to be a unique one + } + } +} + +impl PartialOrd for DateTime { + #[allow(unreachable_patterns)] + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + #[cfg(feature = "time")] + (Self::Time(a), Self::Time(b)) => a.partial_cmp(b), + #[cfg(feature = "chrono")] + (Self::Chrono(a), Self::Chrono(b)) => a.partial_cmp(b), + _ => unimplemented!("Cannot compare time::OffsetDateTime with chrono::DateTime"), + } + } +} + +impl PartialEq for DateTime { + #[allow(unreachable_patterns)] + fn eq(&self, other: &Self) -> bool { + match (self, other) { + #[cfg(feature = "time")] + (Self::Time(self_time), Self::Time(other_time)) => self_time == other_time, + #[cfg(feature = "chrono")] + (Self::Chrono(self_chrono), Self::Chrono(other_chrono)) => self_chrono == other_chrono, + _ => unimplemented!("Cannot compare time::OffsetDateTime with chrono::DateTime"), + } + } +} + +impl DateTime { + #[cfg(feature = "time")] + #[allow(clippy::unnecessary_wraps)] + pub const fn as_time(self) -> Option { + match self { + Self::Time(t) => Some(t), + #[cfg(feature = "chrono")] + Self::Chrono(_) => None, + } + } + + #[cfg(feature = "chrono")] + #[allow(clippy::unnecessary_wraps)] + pub const fn as_chrono(self) -> Option> { + match self { + #[cfg(feature = "time")] + Self::Time(_) => None, + Self::Chrono(t) => Some(t), + } + } + + pub fn weekday(&self) -> Weekday { + match self { + #[cfg(feature = "time")] + Self::Time(t) => t.weekday().into(), + #[cfg(feature = "chrono")] + Self::Chrono(t) => t.weekday().into(), + } + } + + pub fn and_hms(&self, hour: u8, min: u8, sec: u8) -> Self { + match self { + #[cfg(feature = "time")] + Self::Time(t) => Self::Time( + t.replace_time(time::Time::from_hms(hour, min, sec).expect("invalid time")), + ), + #[cfg(feature = "chrono")] + Self::Chrono(t) => Self::Chrono( + t.with_hour(u32::from(hour)) + .expect("invalid hour") + .with_minute(u32::from(min)) + .expect("invalid minute") + .with_second(u32::from(sec)) + .expect("invalid second"), + ), + } + } + + pub fn and_ymd(&self, year: i32, month: u8, day: u8) -> Self { + match self { + #[cfg(feature = "time")] + Self::Time(t) => Self::Time( + t.replace_date( + time::Date::from_calendar_date( + year, + time::Month::try_from(month).expect("invalid month"), + day, + ) + .expect("invalid date"), + ), + ), + #[cfg(feature = "chrono")] + Self::Chrono(t) => Self::Chrono( + t.with_year(year) + .expect("invalid year") + .with_month(u32::from(month)) + .expect("invalid month") + .with_day(u32::from(day)) + .expect("invalid day"), + ), + } + } +} + +#[cfg(feature = "time")] +impl From for DateTime { + fn from(t: time::OffsetDateTime) -> Self { + Self::Time(t.replace_offset(UtcOffset::UTC)) + } +} + +#[cfg(feature = "chrono")] +impl From> for DateTime { + fn from(t: chrono::DateTime) -> Self { + Self::Chrono(t.with_timezone(&chrono::Utc)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Weekday { + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday, +} + +impl Weekday { + pub const fn num_days_from_monday(self) -> u8 { + self as _ + } +} + +#[cfg(feature = "chrono")] +impl From for Weekday { + fn from(weekday: chrono::Weekday) -> Self { + match weekday { + chrono::Weekday::Mon => Self::Monday, + chrono::Weekday::Tue => Self::Tuesday, + chrono::Weekday::Wed => Self::Wednesday, + chrono::Weekday::Thu => Self::Thursday, + chrono::Weekday::Fri => Self::Friday, + chrono::Weekday::Sat => Self::Saturday, + chrono::Weekday::Sun => Self::Sunday, + } + } +} + +#[cfg(feature = "time")] +impl From for Weekday { + fn from(weekday: time::Weekday) -> Self { + match weekday { + time::Weekday::Monday => Self::Monday, + time::Weekday::Tuesday => Self::Tuesday, + time::Weekday::Wednesday => Self::Wednesday, + time::Weekday::Thursday => Self::Thursday, + time::Weekday::Friday => Self::Friday, + time::Weekday::Saturday => Self::Saturday, + time::Weekday::Sunday => Self::Sunday, + } + } +}