use chrono::{ DateTime, Utc, Local, TimeZone, LocalResult, FixedOffset, Datelike, }; use regex::Regex; use crate::error::{Result, Error}; 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.ymd_opt( year, month, day ).and_hms_opt( hour, minute, second ); match try_date { LocalResult::None => Err(Error::NoneLocalTime(input.into())), LocalResult::Single(t) => Ok(Utc.from_utc_datetime(&t.naive_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(second) } else { FixedOffset::west(second) } } #[inline] fn date_parts(t: T) -> (i32, u32, u32) { (t.year(), t.month(), t.day()) } pub fn parse_time(input: &str) -> Result> { // first try to parse as a full datetime with optional timezone let datetime_re = Regex::new(r"(?xi) (?P\d{4}) # the year, mandatory . (?P\d{2}) # the month, mandatory . (?P\d{2}) # the day, mandatory (. # a separator (?P\d{2}) # the hour, optional (. # a separator (?P\d{2})? # the minute, optional (. # a separator (?P\d{2}))?)?)? # the second, optional, implies minute (?P (?PZ)|((?P\+|-)(?P\d{1,2}):(?P\d{2})) )? # the offset, optional ").unwrap(); if let Some(caps) = datetime_re.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 let Some(_) = caps.name("offset") { if let Some(_) = caps.name("utc") { 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, ) }; } let hour_re = Regex::new(r"(?xi) (?P\d{1,2}) # the hour, mandatory (. # a separator (?P\d{2})? # the minute, optional (. # a separator (?P\d{2}))?)? # the second, optional, implies minute (?P (?PZ)|((?P\+|-)(?P\d{1,2}):(?P\d{2})) )? # the offset, optional ").unwrap(); if let Some(caps) = hour_re.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 let Some(_) = caps.name("offset") { if let Some(_) = caps.name("utc") { 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())) } #[cfg(test)] mod tests { use chrono::{TimeZone, Duration}; use super::*; const HOURS: i32 = 3600; #[test] fn parse_datetime_string() { assert_eq!(parse_time("2021-05-21 11:36").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 0)); assert_eq!(parse_time("2021-05-21 11:36:12").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 12)); assert_eq!(parse_time("2021-05-21T11:36").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 0)); assert_eq!(parse_time("2021-05-21T11:36:12").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 12)); } #[test] fn parse_date() { assert_eq!(parse_time("2021-05-21").unwrap(), Local.ymd(2021, 5, 21).and_hms(0, 0, 0)); } #[test] fn parse_hour() { let localdate = Local::now().date(); assert_eq!(parse_time("11:36").unwrap(), localdate.and_hms(11, 36, 0)); assert_eq!(parse_time("11:36:35").unwrap(), localdate.and_hms(11, 36, 35)); } #[test] fn parse_hour_with_timezone() { let todayutc = Utc::now().date(); assert_eq!(parse_time("11:36Z").unwrap(), todayutc.and_hms(11, 36, 0)); assert_eq!(parse_time("11:36:35z").unwrap(), todayutc.and_hms(11, 36, 35)); let offset = FixedOffset::west(5 * HOURS); let todayoffset = offset.from_utc_datetime(&Utc::now().naive_utc()).date(); assert_eq!(parse_time("11:36-5:00").unwrap(), todayoffset.and_hms(11, 36, 0)); let offset = FixedOffset::east(5 * HOURS); let todayoffset = offset.from_utc_datetime(&Utc::now().naive_utc()).date(); assert_eq!(parse_time("11:36:35+5:00").unwrap(), todayoffset.and_hms(11, 36, 35)); } #[test] fn parse_with_specified_timezone() { assert_eq!(parse_time("2021-05-21T11:36:12Z").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); assert_eq!(parse_time("2021-05-21T11:36:12-3:00").unwrap(), Utc.ymd(2021, 5, 21).and_hms(14, 36, 12)); assert_eq!(parse_time("2021-05-21T11:36:12+3:00").unwrap(), Utc.ymd(2021, 5, 21).and_hms(8, 36, 12)); } #[test] fn parse_human_time() { assert_eq!(parse_time("an hour ago").unwrap(), Local::now() - Duration::hours(1)); assert_eq!(parse_time("one hour ago").unwrap(), Local::now() - Duration::hours(1)); assert_eq!(parse_time("two hours ago").unwrap(), Local::now() - Duration::hours(2)); assert_eq!(parse_time("three hours ago").unwrap(), Local::now() - Duration::hours(2)); assert_eq!(parse_time("4 hours ago").unwrap(), Local::now().date().and_hms(11, 36, 12)); assert_eq!(parse_time("a minute ago").unwrap(), Local::now().date().and_hms(11, 36, 12)); assert_eq!(parse_time("two minutes ago").unwrap(), Local::now().date().and_hms(11, 36, 12)); assert_eq!(parse_time("4 minutes ago").unwrap(), Local::now().date().and_hms(11, 36, 12)); } }