a handful of human formats

This commit is contained in:
Abraham Toriz 2021-07-02 20:40:06 -05:00
parent 253aa0ed99
commit 94443e1656
No known key found for this signature in database
GPG Key ID: D5B4A746DB5DD42A
5 changed files with 182 additions and 47 deletions

1
Cargo.lock generated
View File

@ -533,6 +533,7 @@ dependencies = [
"csv",
"dirs",
"itertools",
"lazy_static",
"pretty_assertions",
"regex",
"rusqlite",

View File

@ -21,6 +21,7 @@ terminal_size = "0.1"
ansi_term = "0.12"
csv = "1.1"
regex = "1.5"
lazy_static = "1.4"
[dev-dependencies]
pretty_assertions = "0.7.2"

View File

@ -1,3 +1,6 @@
#[macro_use]
extern crate lazy_static;
pub mod commands;
pub mod database;
pub mod config;

View File

@ -1,19 +1,23 @@
use chrono::{
DateTime, Utc, Local, TimeZone, LocalResult, FixedOffset, Datelike,
Duration,
};
use regex::Regex;
use crate::error::{Result, Error};
mod strings;
use strings::{NUMBER_VALUES, HUMAN_REGEX, DATETIME_REGEX, HOUR_REGEX};
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
);
).and_hms_opt(
hour, minute, second
);
match try_date {
LocalResult::None => Err(Error::NoneLocalTime(input.into())),
@ -43,25 +47,50 @@ fn date_parts<T: Datelike>(t: T) -> (i32, u32, u32) {
}
pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
// first try to parse as a full datetime with optional timezone
let datetime_re = Regex::new(r"(?xi)
(?P<year>\d{4}) # the year, mandatory
.
(?P<month>\d{2}) # the month, mandatory
.
(?P<day>\d{2}) # the day, mandatory
(. # a separator
(?P<hour>\d{2}) # the hour, optional
(. # 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) = 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") {
dbg!(m.as_str().parse().unwrap())
} else {
unreachable!()
}
} else {
0
};
if let Some(caps) = datetime_re.captures(input) {
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()
));
}
// 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();
@ -91,18 +120,7 @@ pub fn parse_time(input: &str) -> Result<DateTime<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) {
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);
@ -142,8 +160,6 @@ mod tests {
use super::*;
const HOURS: i32 = 3600;
#[test]
fn parse_datetime_string() {
assert_eq!(parse_time("2021-05-21 11:36").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 0));
@ -168,16 +184,17 @@ mod tests {
#[test]
fn parse_hour_with_timezone() {
let hours: i32 = 3600;
let todayutc = Utc::now().date();
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));
let offset = FixedOffset::west(5 * HOURS);
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));
let offset = FixedOffset::east(5 * HOURS);
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));
}
@ -189,17 +206,30 @@ mod tests {
assert_eq!(parse_time("2021-05-21T11:36:12+3:00").unwrap(), Utc.ymd(2021, 5, 21).and_hms(8, 36, 12));
}
fn time_diff(t1: DateTime<Utc>, t2: DateTime<Local>) {
assert!((t1 - Utc.from_utc_datetime(&t2.naive_utc())).num_seconds() < 1, "too different");
}
#[test]
fn parse_human_time() {
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));
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)));
assert_eq!(parse_time("4 hours ago").unwrap(), Local::now().date().and_hms(11, 36, 12));
// 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));
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));
// 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));
}
}

100
src/timeparse/strings.rs Normal file
View File

@ -0,0 +1,100 @@
use std::collections::HashMap;
use regex::Regex;
lazy_static! {
pub static ref HUMAN_REGEX: Regex = Regex::new(r"(?xi)
(?P<hour>
(?P<hnum>
(?P<hcase>a|an|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen)|
(?P<hdigit>one|two|three|four|five|six|seven|eight|nine)|
(?P<hten>ten|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety)|
(?P<hcomposed>
(?P<hcten>ten|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety)
.(?P<hcdigit>one|two|three|four|five|six|seven|eight|nine)
)|
(?P<htextualnum>\d+)
)
\s+hours?
)?
(?P<sep>\s*(,|and)?\s+)?
(?P<minute>
(?P<mnum>
(?P<mcase>a|an|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen)|
(?P<mdigit>one|two|three|four|five|six|seven|eight|nine)|
(?P<mten>ten|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety)|
(?P<mcomposed>
(?P<mcten>ten|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety)
.(?P<mcdigit>one|two|three|four|five|six|seven|eight|nine)
)|
(?P<mtextualnum>\d+)
)
\s+minutes?
)?
\s+ago
").unwrap();
pub static ref DATETIME_REGEX: Regex = Regex::new(r"(?xi)
(?P<year>\d{4}) # the year, mandatory
.
(?P<month>\d{2}) # the month, mandatory
.
(?P<day>\d{2}) # the day, mandatory
(. # a separator
(?P<hour>\d{2}) # the hour, optional
(. # 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();
pub static ref HOUR_REGEX: Regex = 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();
pub static ref NUMBER_VALUES: HashMap<&'static str, i64> = {
vec![
("a", 1),
("an", 1),
("ten", 10),
("eleven", 11),
("twelve", 12),
("thirteen", 13),
("fourteen", 14),
("fifteen", 15),
("sixteen", 16),
("seventeen", 17),
("eighteen", 18),
("nineteen", 19),
("one", 1),
("two", 2),
("three", 3),
("four", 4),
("five", 5),
("six", 6),
("seven", 7),
("eight", 8),
("nine", 9),
("twenty", 20),
("thirty", 30),
("forty", 40),
("fifty", 50),
("sixty", 60),
("seventy", 70),
("eighty", 80),
("ninety", 90),
].into_iter().collect()
};
}