tiempo-rs/src/timeparse.rs

260 lines
11 KiB
Rust
Raw Normal View History

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-08-02 19:13:11 -05:00
#[allow(clippy::too_many_arguments)]
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(t.with_timezone(&Utc)),
2021-07-02 18:34:34 -05:00
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) {
2021-08-02 19:10:34 -05:00
let hours = if caps.name("hour").is_some() {
2021-07-02 20:40:06 -05:00
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()]
2021-08-02 19:10:34 -05:00
} else if caps.name("hcomposed").is_some() {
2021-07-02 20:40:06 -05:00
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-12-13 14:01:43 -06:00
} else if caps.name("half").is_some() {
0.5
2021-07-02 20:40:06 -05:00
} else {
unreachable!()
}
} else {
0.
2021-07-02 20:40:06 -05:00
};
2021-08-02 19:10:34 -05:00
let minutes = if caps.name("minute").is_some() {
2021-07-02 20:40:06 -05:00
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()]
2021-08-02 19:10:34 -05:00
} else if caps.name("mcomposed").is_some() {
2021-07-02 20:40:06 -05:00
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.
2021-07-02 20:40:06 -05:00
};
return Ok(Utc.from_utc_datetime(
&(Local::now() - Duration::minutes((hours * 60. + minutes) as i64)).naive_utc()
2021-07-02 20:40:06 -05:00
));
}
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) {
2022-10-31 00:03:11 -06:00
let year: i32 = caps["year"].parse().unwrap();
let month: u32 = caps["month"].parse().unwrap();
let day: u32 = caps["day"].parse().unwrap();
2021-07-02 18:34:34 -05:00
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-08-02 19:10:34 -05:00
return if caps.name("offset").is_some() {
if caps.name("utc").is_some() {
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 {
2022-10-31 00:03:11 -06:00
let hours: i32 = caps["ohour"].parse().unwrap();
let minutes: i32 = caps["omin"].parse().unwrap();
2021-07-02 18:34:34 -05:00
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) {
2022-10-31 00:03:11 -06:00
let hour: u32 = caps["hour"].parse().unwrap();
2021-07-02 18:34:34 -05:00
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-08-02 19:10:34 -05:00
return if caps.name("offset").is_some() {
if caps.name("utc").is_some() {
2021-07-02 18:34:34 -05:00
let (year, month, day) = date_parts(Utc::now());
date_from_parts(
Utc, input, year, month, day, hour, minute, second,
)
} else {
2022-10-31 00:03:11 -06:00
let hours: i32 = caps["ohour"].parse().unwrap();
let minutes: i32 = caps["omin"].parse().unwrap();
2021-07-02 18:34:34 -05:00
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()))
}
pub fn parse_hours(input: &str) -> Result<u16> {
input.parse().map_err(|_| Error::InvalidHours(input.to_string()))
}
2021-07-01 23:42:59 -05:00
#[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>) {
let diff = dbg!(dbg!(t1) - dbg!(Utc.from_utc_datetime(&t2.naive_utc()))).num_seconds().abs();
2021-07-05 12:14:09 -05:00
assert!(diff < 1, "too different: {} s", diff);
2021-07-02 20:40:06 -05:00
}
2021-07-01 23:42:59 -05:00
2021-07-02 20:40:06 -05:00
#[test]
fn parse_human_times() {
2021-07-02 20:40:06 -05:00
// 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));
2022-07-30 09:47:01 -05:00
time_diff(parse_time("15 hours ago").unwrap(), Local::now() - Duration::hours(15));
2021-07-02 20:40:06 -05:00
// 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));
2022-08-28 15:48:47 -05:00
time_diff(parse_time("half an hour ago").unwrap(), Local::now() - Duration::minutes(30));
2021-07-02 20:40:06 -05:00
// mixed
time_diff(parse_time("an hour 10 minutes ago").unwrap(), Local::now() - Duration::minutes(70));
time_diff(parse_time("2 hours five minutes ago").unwrap(), Local::now() - Duration::minutes(125));
2021-12-13 14:20:56 -06:00
time_diff(parse_time("an hour 12 minutes ago").unwrap(), Local::now() - Duration::minutes(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));
2021-12-13 14:20:56 -06:00
time_diff(parse_time("1h 5m ago").unwrap(), Local::now() - Duration::minutes(60 + 5));
time_diff(parse_time("1h5m ago").unwrap(), Local::now() - Duration::minutes(60 + 5));
2021-07-03 15:53:56 -05:00
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-05 12:14:09 -05:00
time_diff(parse_time("two hours and fifteen minuts ago").unwrap(), Local::now() - Duration::minutes(2 * 60 + 15));
time_diff(parse_time("two hours and thirty minuts ago").unwrap(), Local::now() - Duration::minutes(2 * 60 + 30));
// time_diff(parse_time("two and a half hours ago").unwrap(), Local::now() - Duration::minutes(150));
2021-07-01 23:42:59 -05:00
}
}