diff --git a/src/timeparse.rs b/src/timeparse.rs index 71cebff..99a7834 100644 --- a/src/timeparse.rs +++ b/src/timeparse.rs @@ -1,11 +1,50 @@ -use chrono::{DateTime, Utc, Local, TimeZone, LocalResult, FixedOffset}; +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 re = Regex::new(r"(?xi) + let datetime_re = Regex::new(r"(?xi) (?P\d{4}) # the year, mandatory . (?P\d{2}) # the month, mandatory @@ -22,81 +61,75 @@ pub fn parse_time(input: &str) -> Result> { )? # the offset, optional ").unwrap(); - if let Some(caps) = re.captures(input) { + 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") { - // start with a specific offset or utc if let Some(_) = caps.name("utc") { - // start with utc - let try_date = Utc.ymd_opt( - (&caps["year"]).parse().unwrap(), - (&caps["month"]).parse().unwrap(), - (&caps["day"]).parse().unwrap(), - ).and_hms_opt( - caps.name("hour").map(|m| m.as_str().parse().unwrap()).unwrap_or(0), - caps.name("minute").map(|m| m.as_str().parse().unwrap()).unwrap_or(0), - caps.name("second").map(|m| m.as_str().parse().unwrap()).unwrap_or(0), - ); - - match try_date { - LocalResult::None => Err(Error::NoneLocalTime(input.into())), - LocalResult::Single(t) => Ok(t), - LocalResult::Ambiguous(t1, t2) => Err(Error::AmbiguousLocalTime { - orig: input.into(), - t1: t1.naive_utc(), - t2: t2.naive_utc(), - }), - } + date_from_parts( + Utc, input, year, month, day, hour, minute, second, + ) } else { - let mut offset: i32 = (&caps["ohour"]).parse::().unwrap() * 60 * 60; - offset += (&caps["omin"]).parse::().unwrap() * 60; + let hours: i32 = (&caps["ohour"]).parse().unwrap(); + let minutes: i32 = (&caps["omin"]).parse().unwrap(); + let fo = offset_from_parts(&caps["sign"] == "+", hours, minutes); - let fo = if &caps["sign"] == "+" { - FixedOffset::east(offset) - } else { - FixedOffset::west(offset) - }; - - let try_date = fo.ymd_opt( - (&caps["year"]).parse().unwrap(), - (&caps["month"]).parse().unwrap(), - (&caps["day"]).parse().unwrap(), - ).and_hms_opt( - caps.name("hour").map(|m| m.as_str().parse().unwrap()).unwrap_or(0), - caps.name("minute").map(|m| m.as_str().parse().unwrap()).unwrap_or(0), - caps.name("second").map(|m| m.as_str().parse().unwrap()).unwrap_or(0), - ); - - 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(), - }), - } + date_from_parts( + fo, input, year, month, day, hour, minute, second, + ) } } else { // start with a local time - let try_date = Local.ymd_opt( - (&caps["year"]).parse().unwrap(), - (&caps["month"]).parse().unwrap(), - (&caps["day"]).parse().unwrap(), - ).and_hms_opt( - caps.name("hour").map(|m| m.as_str().parse().unwrap()).unwrap_or(0), - caps.name("minute").map(|m| m.as_str().parse().unwrap()).unwrap_or(0), - caps.name("second").map(|m| m.as_str().parse().unwrap()).unwrap_or(0), - ); + date_from_parts( + Local, input, year, month, day, 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(), - }), + 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, + ) }; } @@ -105,47 +138,48 @@ pub fn parse_time(input: &str) -> Result> { #[cfg(test)] mod tests { - use chrono::TimeZone; + use chrono::{TimeZone, Duration}; use super::*; + const HOURS: i32 = 3600; + #[test] fn parse_datetime_string() { - std::env::set_var("TZ", "America/Mexico_City"); + 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-21 11:36").unwrap(), Utc.ymd(2021, 5, 21).and_hms(16, 36, 0)); - assert_eq!(parse_time("2021-05-21 11:36:12").unwrap(), Utc.ymd(2021, 5, 21).and_hms(16, 36, 12)); - - assert_eq!(parse_time("2021-05-21T11:36").unwrap(), Utc.ymd(2021, 5, 21).and_hms(16, 36, 0)); - assert_eq!(parse_time("2021-05-21T11:36:12").unwrap(), Utc.ymd(2021, 5, 21).and_hms(16, 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(), Utc.ymd(2021, 5, 21).and_hms(5, 0, 0)); + assert_eq!(parse_time("2021-05-21").unwrap(), Local.ymd(2021, 5, 21).and_hms(0, 0, 0)); } #[test] fn parse_hour() { - std::env::set_var("TZ", "America/Mexico_City"); + let localdate = Local::now().date(); - let now = Utc::now(); - - assert_eq!(parse_time("11:36").unwrap(), now.date().and_hms(16, 36, 0)); - assert_eq!(parse_time("11:36:35").unwrap(), now.date().and_hms(16, 36, 35)); + 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() { - std::env::set_var("TZ", "America/Mexico_City"); + let todayutc = Utc::now().date(); - let now = Utc::now(); + 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)); - assert_eq!(parse_time("11:36Z").unwrap(), now.date().and_hms(11, 36, 0)); - assert_eq!(parse_time("11:36:35z").unwrap(), now.date().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)); - assert_eq!(parse_time("11:36-5:00").unwrap(), now.date().and_hms(16, 36, 0)); - assert_eq!(parse_time("11:36:35+5:00").unwrap(), now.date().and_hms(6, 36, 35)); + 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] @@ -157,20 +191,15 @@ mod tests { #[test] fn parse_human_time() { - assert_eq!(parse_time("an hour ago").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); - assert_eq!(parse_time("two hours ago").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); - assert_eq!(parse_time("4 hours ago").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + 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("a minute ago").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); - assert_eq!(parse_time("two minutes ago").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); - assert_eq!(parse_time("4 minutes ago").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + assert_eq!(parse_time("4 hours ago").unwrap(), Local::now().date().and_hms(11, 36, 12)); - assert_eq!(parse_time("hace una hora").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); - assert_eq!(parse_time("hace dos horas").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); - assert_eq!(parse_time("hace cuatro horas").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); - - assert_eq!(parse_time("hace una minuto").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); - assert_eq!(parse_time("hace dos minutos").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); - assert_eq!(parse_time("hace cuatro minutos").unwrap(), Utc.ymd(2021, 5, 21).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)); } }