a handful of human formats
This commit is contained in:
parent
253aa0ed99
commit
94443e1656
|
@ -533,6 +533,7 @@ dependencies = [
|
||||||
"csv",
|
"csv",
|
||||||
"dirs",
|
"dirs",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
"lazy_static",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"regex",
|
"regex",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
|
|
@ -21,6 +21,7 @@ terminal_size = "0.1"
|
||||||
ansi_term = "0.12"
|
ansi_term = "0.12"
|
||||||
csv = "1.1"
|
csv = "1.1"
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
|
lazy_static = "1.4"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "0.7.2"
|
pretty_assertions = "0.7.2"
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
118
src/timeparse.rs
118
src/timeparse.rs
|
@ -1,10 +1,14 @@
|
||||||
use chrono::{
|
use chrono::{
|
||||||
DateTime, Utc, Local, TimeZone, LocalResult, FixedOffset, Datelike,
|
DateTime, Utc, Local, TimeZone, LocalResult, FixedOffset, Datelike,
|
||||||
|
Duration,
|
||||||
};
|
};
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use crate::error::{Result, Error};
|
use crate::error::{Result, Error};
|
||||||
|
|
||||||
|
mod strings;
|
||||||
|
|
||||||
|
use strings::{NUMBER_VALUES, HUMAN_REGEX, DATETIME_REGEX, HOUR_REGEX};
|
||||||
|
|
||||||
fn date_from_parts<T: TimeZone>(
|
fn date_from_parts<T: TimeZone>(
|
||||||
timezone: T, input: &str, year: i32, month: u32, day: u32, hour: u32,
|
timezone: T, input: &str, year: i32, month: u32, day: u32, hour: u32,
|
||||||
minute: u32, second: u32
|
minute: u32, second: u32
|
||||||
|
@ -43,25 +47,50 @@ fn date_parts<T: Datelike>(t: T) -> (i32, u32, u32) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
|
pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
|
||||||
// first try to parse as a full datetime with optional timezone
|
if let Some(caps) = HUMAN_REGEX.captures(input) {
|
||||||
let datetime_re = Regex::new(r"(?xi)
|
let hours = if let Some(_) = caps.name("hour") {
|
||||||
(?P<year>\d{4}) # the year, mandatory
|
if let Some(m) = caps.name("hcase") {
|
||||||
.
|
NUMBER_VALUES[m.as_str()]
|
||||||
(?P<month>\d{2}) # the month, mandatory
|
} else if let Some(m) = caps.name("hdigit") {
|
||||||
.
|
NUMBER_VALUES[m.as_str()]
|
||||||
(?P<day>\d{2}) # the day, mandatory
|
} else if let Some(m) = caps.name("hten") {
|
||||||
(. # a separator
|
NUMBER_VALUES[m.as_str()]
|
||||||
(?P<hour>\d{2}) # the hour, optional
|
} else if let Some(_) = caps.name("hcomposed") {
|
||||||
(. # a separator
|
NUMBER_VALUES[&caps["hcten"]] + NUMBER_VALUES[&caps["hcdigit"]]
|
||||||
(?P<minute>\d{2})? # the minute, optional
|
} else if let Some(m) = caps.name("htextualnum") {
|
||||||
(. # a separator
|
dbg!(m.as_str().parse().unwrap())
|
||||||
(?P<second>\d{2}))?)?)? # the second, optional, implies minute
|
} else {
|
||||||
(?P<offset>
|
unreachable!()
|
||||||
(?P<utc>Z)|((?P<sign>\+|-)(?P<ohour>\d{1,2}):(?P<omin>\d{2}))
|
}
|
||||||
)? # the offset, optional
|
} else {
|
||||||
").unwrap();
|
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 year: i32 = (&caps["year"]).parse().unwrap();
|
||||||
let month: u32 = (&caps["month"]).parse().unwrap();
|
let month: u32 = (&caps["month"]).parse().unwrap();
|
||||||
let day: u32 = (&caps["day"]).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)
|
if let Some(caps) = HOUR_REGEX.captures(input) {
|
||||||
(?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 hour: u32 = (&caps["hour"]).parse().unwrap();
|
||||||
let minute: u32 = caps.name("minute").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);
|
let second: u32 = caps.name("second").map(|t| t.as_str().parse().unwrap()).unwrap_or(0);
|
||||||
|
@ -142,8 +160,6 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const HOURS: i32 = 3600;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_datetime_string() {
|
fn parse_datetime_string() {
|
||||||
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").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 0));
|
||||||
|
@ -168,16 +184,17 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_hour_with_timezone() {
|
fn parse_hour_with_timezone() {
|
||||||
|
let hours: i32 = 3600;
|
||||||
let todayutc = Utc::now().date();
|
let todayutc = Utc::now().date();
|
||||||
|
|
||||||
assert_eq!(parse_time("11:36Z").unwrap(), todayutc.and_hms(11, 36, 0));
|
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: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();
|
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(), 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();
|
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));
|
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));
|
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]
|
#[test]
|
||||||
fn parse_human_time() {
|
fn parse_human_minute() {
|
||||||
assert_eq!(parse_time("an hour ago").unwrap(), Local::now() - Duration::hours(1));
|
// hours
|
||||||
assert_eq!(parse_time("one hour ago").unwrap(), Local::now() - Duration::hours(1));
|
time_diff(parse_time("an hour ago").unwrap(), Local::now() - Duration::hours(1));
|
||||||
assert_eq!(parse_time("two hours ago").unwrap(), Local::now() - Duration::hours(2));
|
time_diff(parse_time("two hours ago").unwrap(), Local::now() - Duration::hours(2));
|
||||||
assert_eq!(parse_time("three 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));
|
// mixed
|
||||||
assert_eq!(parse_time("two minutes ago").unwrap(), Local::now().date().and_hms(11, 36, 12));
|
time_diff(parse_time("an hour 10 minutes ago").unwrap(), Local::now() - Duration::minutes(1));
|
||||||
assert_eq!(parse_time("4 minutes ago").unwrap(), Local::now().date().and_hms(11, 36, 12));
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue