tiempo-rs/src/timeparse.rs

258 lines
11 KiB
Rust

use chrono::{
DateTime, Utc, Local, TimeZone, LocalResult, FixedOffset, Datelike,
Duration,
};
use crate::error::{Result, Error};
mod strings;
use strings::{NUMBER_VALUES, HUMAN_REGEX, DATETIME_REGEX, HOUR_REGEX};
#[allow(clippy::too_many_arguments)]
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.with_ymd_and_hms(
year, month, day, hour, minute, second
);
match try_date {
LocalResult::None => Err(Error::NoneLocalTime(input.into())),
LocalResult::Single(t) => Ok(t.with_timezone(&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_opt(second).unwrap()
} else {
FixedOffset::west_opt(second).unwrap()
}
}
#[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>> {
if let Some(caps) = HUMAN_REGEX.captures(input) {
let hours = if caps.name("hour").is_some() {
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 caps.name("hcomposed").is_some() {
NUMBER_VALUES[&caps["hcten"]] + NUMBER_VALUES[&caps["hcdigit"]]
} else if let Some(m) = caps.name("htextualnum") {
m.as_str().parse().unwrap()
} else if caps.name("half").is_some() {
0.5
} else {
unreachable!()
}
} else {
0.
};
let minutes = if caps.name("minute").is_some() {
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 caps.name("mcomposed").is_some() {
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) as i64)).naive_utc()
));
}
// first try to parse as a full datetime with optional timezone
if let Some(caps) = DATETIME_REGEX.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 caps.name("offset").is_some() {
if caps.name("utc").is_some() {
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);
date_from_parts(
fo, input, year, month, day, hour, minute, second,
)
}
} else {
// start with a local time
date_from_parts(
Local, input, year, month, day, hour, minute, second,
)
};
}
if let Some(caps) = HOUR_REGEX.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 caps.name("offset").is_some() {
if caps.name("utc").is_some() {
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,
)
};
}
Err(Error::DateTimeParseError(input.into()))
}
pub fn parse_hours(input: &str) -> Result<u16> {
input.parse().map_err(|_| Error::InvalidHours(input.to_string()))
}
#[cfg(test)]
mod tests {
use chrono::{TimeZone, Duration, Timelike};
use super::*;
#[test]
fn parse_datetime_string() {
assert_eq!(parse_time("2021-05-21 11:36").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 0).unwrap());
assert_eq!(parse_time("2021-05-21 11:36:12").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap());
assert_eq!(parse_time("2021-05-21T11:36").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 0).unwrap());
assert_eq!(parse_time("2021-05-21T11:36:12").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap());
}
#[test]
fn parse_date() {
assert_eq!(parse_time("2021-05-21").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 0, 0, 0).unwrap());
}
#[test]
fn parse_hour() {
let localdate = Local::now().with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap();
assert_eq!(parse_time("11:36").unwrap(), localdate.with_hour(11).unwrap().with_minute(36).unwrap().with_timezone(&Utc));
assert_eq!(parse_time("11:36:35").unwrap(), localdate.with_hour(11).unwrap().with_minute(36).unwrap().with_second(35).unwrap().with_timezone(&Utc));
}
#[test]
fn parse_hour_with_timezone() {
let hours: i32 = 3600;
let todayutc = Utc::now().date_naive();
assert_eq!(parse_time("11:36Z").unwrap(), todayutc.and_hms_opt(11, 36, 0).unwrap().and_local_timezone(Utc).unwrap());
assert_eq!(parse_time("11:36:35z").unwrap(), todayutc.and_hms_opt(11, 36, 35).unwrap().and_local_timezone(Utc).unwrap());
let offset = FixedOffset::west_opt(5 * hours).unwrap();
let today_at_offset = offset.from_utc_datetime(&Utc::now().naive_utc()).with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap();
assert_eq!(parse_time("11:36-5:00").unwrap(), today_at_offset.with_hour(11).unwrap().with_minute(36).unwrap().with_timezone(&Utc));
let offset = FixedOffset::east_opt(5 * hours).unwrap();
let today_at_offset = offset.from_utc_datetime(&Utc::now().naive_utc()).with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap();
assert_eq!(parse_time("11:36:35+5:00").unwrap(), today_at_offset.with_hour(11).unwrap().with_minute(36).unwrap().with_second(35).unwrap().with_timezone(&Utc));
}
#[test]
fn parse_with_specified_timezone() {
assert_eq!(parse_time("2021-05-21T11:36:12Z").unwrap(), Utc.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap());
assert_eq!(parse_time("2021-05-21T11:36:12-3:00").unwrap(), Utc.with_ymd_and_hms(2021, 5, 21, 14, 36, 12).unwrap());
assert_eq!(parse_time("2021-05-21T11:36:12+3:00").unwrap(), Utc.with_ymd_and_hms(2021, 5, 21, 8, 36, 12).unwrap());
}
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();
assert!(diff < 1, "too different: {} s", diff);
}
#[test]
fn parse_human_times() {
// 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(parse_time("15 hours ago").unwrap(), 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));
time_diff(parse_time("half an hour ago").unwrap(), Local::now() - Duration::minutes(30));
// 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));
time_diff(parse_time("an hour 12 minutes ago").unwrap(), Local::now() - Duration::minutes(60 + 12));
// 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(60 + 5));
time_diff(parse_time("1h5m ago").unwrap(), Local::now() - Duration::minutes(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));
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));
}
}