2021-07-02 18:34:34 -05:00
|
|
|
use chrono::{
|
|
|
|
DateTime, Utc, Local, TimeZone, LocalResult, FixedOffset, Datelike,
|
2021-07-02 20:40:06 -05:00
|
|
|
Duration,
|
2021-07-02 18:34:34 -05:00
|
|
|
};
|
2021-07-01 23:42:59 -05:00
|
|
|
|
|
|
|
use crate::error::{Result, Error};
|
|
|
|
|
2021-07-02 20:40:06 -05:00
|
|
|
mod strings;
|
|
|
|
|
|
|
|
use strings::{NUMBER_VALUES, HUMAN_REGEX, DATETIME_REGEX, HOUR_REGEX};
|
|
|
|
|
2021-07-02 18:34:34 -05:00
|
|
|
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
|
2021-07-02 20:40:06 -05:00
|
|
|
).and_hms_opt(
|
|
|
|
hour, minute, second
|
|
|
|
);
|
2021-07-02 18:34:34 -05:00
|
|
|
|
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2021-07-01 23:42:59 -05:00
|
|
|
pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
|
2021-07-02 20:40:06 -05:00
|
|
|
if let Some(caps) = HUMAN_REGEX.captures(input) {
|
|
|
|
let hours = if let Some(_) = caps.name("hour") {
|
|
|
|
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 let Some(_) = caps.name("hcomposed") {
|
|
|
|
NUMBER_VALUES[&caps["hcten"]] + NUMBER_VALUES[&caps["hcdigit"]]
|
|
|
|
} else if let Some(m) = caps.name("htextualnum") {
|
2021-07-03 18:20:35 -05:00
|
|
|
m.as_str().parse().unwrap()
|
2021-07-02 20:40:06 -05:00
|
|
|
} else {
|
|
|
|
unreachable!()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
0
|
|
|
|
};
|
|
|
|
|
|
|
|
let minutes = if let Some(_) = caps.name("minute") {
|
|
|
|
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 let Some(_) = caps.name("mcomposed") {
|
|
|
|
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)).naive_utc()
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2021-07-01 23:42:59 -05:00
|
|
|
// first try to parse as a full datetime with optional timezone
|
2021-07-02 20:40:06 -05:00
|
|
|
if let Some(caps) = DATETIME_REGEX.captures(input) {
|
2021-07-02 18:34:34 -05:00
|
|
|
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);
|
|
|
|
|
2021-07-02 10:07:07 -05:00
|
|
|
return if let Some(_) = caps.name("offset") {
|
2021-07-01 23:42:59 -05:00
|
|
|
if let Some(_) = caps.name("utc") {
|
2021-07-02 18:34:34 -05:00
|
|
|
date_from_parts(
|
|
|
|
Utc, input, year, month, day, hour, minute, second,
|
|
|
|
)
|
2021-07-01 23:42:59 -05:00
|
|
|
} else {
|
2021-07-02 18:34:34 -05:00
|
|
|
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,
|
|
|
|
)
|
2021-07-01 23:42:59 -05:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// start with a local time
|
2021-07-02 18:34:34 -05:00
|
|
|
date_from_parts(
|
|
|
|
Local, input, year, month, day, hour, minute, second,
|
|
|
|
)
|
|
|
|
};
|
|
|
|
}
|
2021-07-01 23:42:59 -05:00
|
|
|
|
2021-07-02 20:40:06 -05:00
|
|
|
if let Some(caps) = HOUR_REGEX.captures(input) {
|
2021-07-02 18:34:34 -05:00
|
|
|
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,
|
|
|
|
)
|
2021-07-02 10:07:07 -05:00
|
|
|
}
|
2021-07-02 18:34:34 -05:00
|
|
|
} else {
|
|
|
|
let (year, month, day) = date_parts(Local::now());
|
|
|
|
|
|
|
|
date_from_parts(
|
|
|
|
Local, input, year, month, day, hour, minute, second,
|
|
|
|
)
|
2021-07-02 10:07:07 -05:00
|
|
|
};
|
2021-07-01 23:42:59 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
Err(Error::DateTimeParseError(input.into()))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2021-07-02 18:34:34 -05:00
|
|
|
use chrono::{TimeZone, Duration};
|
2021-07-01 23:42:59 -05:00
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn parse_datetime_string() {
|
2021-07-02 18:34:34 -05:00
|
|
|
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));
|
2021-07-01 23:42:59 -05:00
|
|
|
|
2021-07-02 18:34:34 -05:00
|
|
|
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));
|
2021-07-01 23:42:59 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn parse_date() {
|
2021-07-02 18:34:34 -05:00
|
|
|
assert_eq!(parse_time("2021-05-21").unwrap(), Local.ymd(2021, 5, 21).and_hms(0, 0, 0));
|
2021-07-01 23:42:59 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn parse_hour() {
|
2021-07-02 18:34:34 -05:00
|
|
|
let localdate = Local::now().date();
|
2021-07-01 23:42:59 -05:00
|
|
|
|
2021-07-02 18:34:34 -05:00
|
|
|
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));
|
2021-07-01 23:42:59 -05:00
|
|
|
}
|
|
|
|
|
2021-07-02 10:07:07 -05:00
|
|
|
#[test]
|
|
|
|
fn parse_hour_with_timezone() {
|
2021-07-02 20:40:06 -05:00
|
|
|
let hours: i32 = 3600;
|
2021-07-02 18:34:34 -05:00
|
|
|
let todayutc = Utc::now().date();
|
2021-07-02 10:07:07 -05:00
|
|
|
|
2021-07-02 18:34:34 -05:00
|
|
|
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));
|
2021-07-02 10:07:07 -05:00
|
|
|
|
2021-07-02 20:40:06 -05:00
|
|
|
let offset = FixedOffset::west(5 * hours);
|
2021-07-02 18:34:34 -05:00
|
|
|
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));
|
2021-07-02 10:07:07 -05:00
|
|
|
|
2021-07-02 20:40:06 -05:00
|
|
|
let offset = FixedOffset::east(5 * hours);
|
2021-07-02 18:34:34 -05:00
|
|
|
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));
|
2021-07-02 10:07:07 -05:00
|
|
|
}
|
|
|
|
|
2021-07-01 23:42:59 -05:00
|
|
|
#[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));
|
2021-07-02 10:07:07 -05:00
|
|
|
assert_eq!(parse_time("2021-05-21T11:36:12+3:00").unwrap(), Utc.ymd(2021, 5, 21).and_hms(8, 36, 12));
|
2021-07-01 23:42:59 -05:00
|
|
|
}
|
|
|
|
|
2021-07-02 20:40:06 -05:00
|
|
|
fn time_diff(t1: DateTime<Utc>, t2: DateTime<Local>) {
|
|
|
|
assert!((t1 - Utc.from_utc_datetime(&t2.naive_utc())).num_seconds() < 1, "too different");
|
|
|
|
}
|
2021-07-01 23:42:59 -05:00
|
|
|
|
2021-07-02 20:40:06 -05:00
|
|
|
#[test]
|
|
|
|
fn parse_human_minute() {
|
|
|
|
// 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(dbg!(parse_time("15 hours ago").unwrap()), dbg!(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));
|
|
|
|
|
|
|
|
// mixed
|
|
|
|
time_diff(parse_time("an hour 10 minutes ago").unwrap(), Local::now() - Duration::minutes(1));
|
|
|
|
time_diff(parse_time("2 hours five minutes ago").unwrap(), Local::now() - Duration::minutes(1));
|
|
|
|
time_diff(parse_time("an hour 12 minutes ago").unwrap(), Local::now() - Duration::minutes(1 * 60 + 12));
|
2021-07-03 15:53:56 -05:00
|
|
|
|
|
|
|
// 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(1 * 60 + 5));
|
|
|
|
time_diff(parse_time("1h5m ago").unwrap(), Local::now() - Duration::minutes(1 * 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));
|
2021-07-01 23:42:59 -05:00
|
|
|
}
|
|
|
|
}
|