use chrono::{ DateTime, Utc, Local, TimeZone, LocalResult, FixedOffset, Datelike, Duration, }; use crate::error::{Result, Error}; mod strings; use strings::{NUMBER_VALUES, HUMAN_REGEX, DATETIME_REGEX, HOUR_REGEX}; #[allow(clippy::too_many_arguments)] fn date_from_parts( timezone: T, input: &str, year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32 ) -> Result> { let try_date = timezone.with_ymd_and_hms( year, month, day, hour, minute, second ); match try_date { LocalResult::None => Err(Error::NoneLocalTime(input.into())), LocalResult::Single(t) => Ok(t.with_timezone(&Utc)), LocalResult::Ambiguous(t1, t2) => Err(Error::AmbiguousLocalTime { orig: input.into(), t1: t1.naive_utc(), t2: t2.naive_utc(), }), } } fn offset_from_parts(east: bool, hours: i32, minutes: i32) -> FixedOffset { let mut second: i32 = hours * 60 * 60; second += minutes * 60; if east { FixedOffset::east_opt(second).unwrap() } else { FixedOffset::west_opt(second).unwrap() } } #[inline] fn date_parts(t: T) -> (i32, u32, u32) { (t.year(), t.month(), t.day()) } pub fn parse_time(input: &str) -> Result> { if let Some(caps) = HUMAN_REGEX.captures(input) { let hours = if caps.name("hour").is_some() { if let Some(m) = caps.name("hcase") { NUMBER_VALUES[m.as_str()] } else if let Some(m) = caps.name("hdigit") { NUMBER_VALUES[m.as_str()] } else if let Some(m) = caps.name("hten") { NUMBER_VALUES[m.as_str()] } else if caps.name("hcomposed").is_some() { NUMBER_VALUES[&caps["hcten"]] + NUMBER_VALUES[&caps["hcdigit"]] } else if let Some(m) = caps.name("htextualnum") { m.as_str().parse().unwrap() } else if caps.name("half").is_some() { 0.5 } else { unreachable!() } } else { 0. }; let minutes = if caps.name("minute").is_some() { if let Some(m) = caps.name("mcase") { NUMBER_VALUES[m.as_str()] } else if let Some(m) = caps.name("mdigit") { NUMBER_VALUES[m.as_str()] } else if let Some(m) = caps.name("mten") { NUMBER_VALUES[m.as_str()] } else if caps.name("mcomposed").is_some() { NUMBER_VALUES[&caps["mcten"]] + NUMBER_VALUES[&caps["mcdigit"]] } else if let Some(m) = caps.name("mtextualnum") { m.as_str().parse().unwrap() } else { unreachable!() } } else { 0. }; return Ok(Utc.from_utc_datetime( &(Local::now() - Duration::minutes((hours * 60. + minutes) as i64)).naive_utc() )); } // first try to parse as a full datetime with optional timezone if let Some(caps) = DATETIME_REGEX.captures(input) { let year: i32 = caps["year"].parse().unwrap(); let month: u32 = caps["month"].parse().unwrap(); let day: u32 = caps["day"].parse().unwrap(); let hour: u32 = caps.name("hour").map(|t| t.as_str().parse().unwrap()).unwrap_or(0); let minute: u32 = caps.name("minute").map(|t| t.as_str().parse().unwrap()).unwrap_or(0); let second: u32 = caps.name("second").map(|t| t.as_str().parse().unwrap()).unwrap_or(0); return if caps.name("offset").is_some() { if caps.name("utc").is_some() { date_from_parts( Utc, input, year, month, day, hour, minute, second, ) } else { let hours: i32 = caps["ohour"].parse().unwrap(); let minutes: i32 = caps["omin"].parse().unwrap(); let fo = offset_from_parts(&caps["sign"] == "+", hours, minutes); date_from_parts( fo, input, year, month, day, hour, minute, second, ) } } else { // start with a local time date_from_parts( Local, input, year, month, day, hour, minute, second, ) }; } if let Some(caps) = HOUR_REGEX.captures(input) { let hour: u32 = caps["hour"].parse().unwrap(); let minute: u32 = caps.name("minute").map(|t| t.as_str().parse().unwrap()).unwrap_or(0); let second: u32 = caps.name("second").map(|t| t.as_str().parse().unwrap()).unwrap_or(0); return if caps.name("offset").is_some() { if caps.name("utc").is_some() { let (year, month, day) = date_parts(Utc::now()); date_from_parts( Utc, input, year, month, day, hour, minute, second, ) } else { let hours: i32 = caps["ohour"].parse().unwrap(); let minutes: i32 = caps["omin"].parse().unwrap(); let fo = offset_from_parts(&caps["sign"] == "+", hours, minutes); let (year, month, day) = date_parts(fo.from_utc_datetime(&Utc::now().naive_utc())); date_from_parts( fo, input, year, month, day, hour, minute, second, ) } } else { let (year, month, day) = date_parts(Local::now()); date_from_parts( Local, input, year, month, day, hour, minute, second, ) }; } Err(Error::DateTimeParseError(input.into())) } pub fn parse_hours(input: &str) -> Result { input.parse().map_err(|_| Error::InvalidHours(input.to_string())) } #[cfg(test)] mod tests { use chrono::{TimeZone, Duration, Timelike}; use super::*; #[test] fn parse_datetime_string() { assert_eq!(parse_time("2021-05-21 11:36").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 0).unwrap()); assert_eq!(parse_time("2021-05-21 11:36:12").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap()); assert_eq!(parse_time("2021-05-21T11:36").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 0).unwrap()); assert_eq!(parse_time("2021-05-21T11:36:12").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap()); } #[test] fn parse_date() { assert_eq!(parse_time("2021-05-21").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 0, 0, 0).unwrap()); } #[test] fn parse_hour() { let localdate = Local::now().with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap(); assert_eq!(parse_time("11:36").unwrap(), localdate.with_hour(11).unwrap().with_minute(36).unwrap().with_timezone(&Utc)); assert_eq!(parse_time("11:36:35").unwrap(), localdate.with_hour(11).unwrap().with_minute(36).unwrap().with_second(35).unwrap().with_timezone(&Utc)); } #[test] fn parse_hour_with_timezone() { let hours: i32 = 3600; let todayutc = Utc::now().date_naive(); assert_eq!(parse_time("11:36Z").unwrap(), todayutc.and_hms_opt(11, 36, 0).unwrap().and_local_timezone(Utc).unwrap()); assert_eq!(parse_time("11:36:35z").unwrap(), todayutc.and_hms_opt(11, 36, 35).unwrap().and_local_timezone(Utc).unwrap()); let offset = FixedOffset::west_opt(5 * hours).unwrap(); let today_at_offset = offset.from_utc_datetime(&Utc::now().naive_utc()).with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap(); assert_eq!(parse_time("11:36-5:00").unwrap(), today_at_offset.with_hour(11).unwrap().with_minute(36).unwrap().with_timezone(&Utc)); let offset = FixedOffset::east_opt(5 * hours).unwrap(); let today_at_offset = offset.from_utc_datetime(&Utc::now().naive_utc()).with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap(); assert_eq!(parse_time("11:36:35+5:00").unwrap(), today_at_offset.with_hour(11).unwrap().with_minute(36).unwrap().with_second(35).unwrap().with_timezone(&Utc)); } #[test] fn parse_with_specified_timezone() { assert_eq!(parse_time("2021-05-21T11:36:12Z").unwrap(), Utc.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap()); assert_eq!(parse_time("2021-05-21T11:36:12-3:00").unwrap(), Utc.with_ymd_and_hms(2021, 5, 21, 14, 36, 12).unwrap()); assert_eq!(parse_time("2021-05-21T11:36:12+3:00").unwrap(), Utc.with_ymd_and_hms(2021, 5, 21, 8, 36, 12).unwrap()); } fn time_diff(t1: DateTime, t2: DateTime) { let diff = dbg!(dbg!(t1) - dbg!(Utc.from_utc_datetime(&t2.naive_utc()))).num_seconds().abs(); assert!(diff < 1, "too different: {} s", diff); } #[test] fn parse_human_times() { // hours time_diff(parse_time("an hour ago").unwrap(), Local::now() - Duration::hours(1)); time_diff(parse_time("two hours ago").unwrap(), Local::now() - Duration::hours(2)); time_diff(parse_time("ten hours ago").unwrap(), Local::now() - Duration::hours(10)); time_diff(parse_time("twenty one hours ago").unwrap(), Local::now() - Duration::hours(21)); time_diff(parse_time("15 hours ago").unwrap(), Local::now() - Duration::hours(15)); // minutes time_diff(parse_time("a minute ago").unwrap(), Local::now() - Duration::minutes(1)); time_diff(parse_time("two minutes ago").unwrap(), Local::now() - Duration::minutes(2)); time_diff(parse_time("thirty minutes ago").unwrap(), Local::now() - Duration::minutes(30)); time_diff(parse_time("forty one minutes ago").unwrap(), Local::now() - Duration::minutes(41)); time_diff(parse_time("1 minute ago").unwrap(), Local::now() - Duration::minutes(1)); time_diff(parse_time("23 minutes ago").unwrap(), Local::now() - Duration::minutes(23)); time_diff(parse_time("half an hour ago").unwrap(), Local::now() - Duration::minutes(30)); // mixed time_diff(parse_time("an hour 10 minutes ago").unwrap(), Local::now() - Duration::minutes(70)); time_diff(parse_time("2 hours five minutes ago").unwrap(), Local::now() - Duration::minutes(125)); time_diff(parse_time("an hour 12 minutes ago").unwrap(), Local::now() - Duration::minutes(60 + 12)); // abbreviated time_diff(parse_time("2hrs ago").unwrap(), Local::now() - Duration::hours(2)); time_diff(parse_time("10min ago").unwrap(), Local::now() - Duration::minutes(10)); time_diff(parse_time("1hr ago").unwrap(), Local::now() - Duration::hours(1)); time_diff(parse_time("1h ago").unwrap(), Local::now() - Duration::hours(1)); time_diff(parse_time("1h 5m ago").unwrap(), Local::now() - Duration::minutes(60 + 5)); time_diff(parse_time("1h5m ago").unwrap(), Local::now() - Duration::minutes(60 + 5)); time_diff(parse_time("a m ago").unwrap(), Local::now() - Duration::minutes(1)); time_diff(parse_time("an hr ago").unwrap(), Local::now() - Duration::hours(1)); time_diff(parse_time("a min ago").unwrap(), Local::now() - Duration::minutes(1)); time_diff(parse_time("two hours and fifteen minuts ago").unwrap(), Local::now() - Duration::minutes(2 * 60 + 15)); time_diff(parse_time("two hours and thirty minuts ago").unwrap(), Local::now() - Duration::minutes(2 * 60 + 30)); // time_diff(parse_time("two and a half hours ago").unwrap(), Local::now() - Duration::minutes(150)); } }