use chrono::{DateTime, Utc, Local, TimeZone, LocalResult, FixedOffset}; use regex::Regex; use crate::error::{Result, Error}; pub fn parse_time(input: &str) -> Result> { // first try to parse as a full datetime with optional timezone let 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) = re.captures(input) { 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(), }), } } else { let mut offset: i32 = (&caps["ohour"]).parse::().unwrap() * 60 * 60; offset += (&caps["omin"]).parse::().unwrap() * 60; 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(), }), } } } 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), ); 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(), }), } }; } Err(Error::DateTimeParseError(input.into())) } #[cfg(test)] mod tests { use chrono::TimeZone; use super::*; #[test] fn parse_datetime_string() { std::env::set_var("TZ", "America/Mexico_City"); 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)); } #[test] fn parse_date() { assert_eq!(parse_time("2021-05-21").unwrap(), Utc.ymd(2021, 5, 21).and_hms(5, 0, 0)); } #[test] fn parse_hour() { std::env::set_var("TZ", "America/Mexico_City"); 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)); } #[test] fn parse_hour_with_timezone() { std::env::set_var("TZ", "America/Mexico_City"); let now = Utc::now(); 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)); 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)); } #[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(), 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("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("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)); } }