parse dates and times precisely
This commit is contained in:
parent
a56df91b58
commit
253aa0ed99
229
src/timeparse.rs
229
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<T: TimeZone>(
|
||||
timezone: T, input: &str, year: i32, month: u32, day: u32, hour: u32,
|
||||
minute: u32, second: u32
|
||||
) -> Result<DateTime<Utc>> {
|
||||
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: Datelike>(t: T) -> (i32, u32, u32) {
|
||||
(t.year(), t.month(), t.day())
|
||||
}
|
||||
|
||||
pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
|
||||
// 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<year>\d{4}) # the year, mandatory
|
||||
.
|
||||
(?P<month>\d{2}) # the month, mandatory
|
||||
|
@ -22,81 +61,75 @@ pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
|
|||
)? # 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::<i32>().unwrap() * 60 * 60;
|
||||
offset += (&caps["omin"]).parse::<i32>().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<hour>\d{1,2}) # the hour, mandatory
|
||||
(. # a separator
|
||||
(?P<minute>\d{2})? # the minute, optional
|
||||
(. # a separator
|
||||
(?P<second>\d{2}))?)? # the second, optional, implies minute
|
||||
(?P<offset>
|
||||
(?P<utc>Z)|((?P<sign>\+|-)(?P<ohour>\d{1,2}):(?P<omin>\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<DateTime<Utc>> {
|
|||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue