From 3dfefedb751735c3040e3141fdd6904e7eea0f06 Mon Sep 17 00:00:00 2001 From: Abraham Toriz Date: Thu, 1 Jul 2021 23:42:59 -0500 Subject: [PATCH] introducing the timeparse module --- Cargo.lock | 1 + Cargo.toml | 1 + src/commands/display.rs | 9 ++-- src/commands/in.rs | 3 +- src/error.rs | 8 +-- src/lib.rs | 1 + src/timeparse.rs | 115 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 src/timeparse.rs diff --git a/Cargo.lock b/Cargo.lock index b18d7db..3589522 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -534,6 +534,7 @@ dependencies = [ "dirs", "itertools", "pretty_assertions", + "regex", "rusqlite", "serde", "serde_yaml", diff --git a/Cargo.toml b/Cargo.toml index 2338df6..64b0e48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ textwrap = "0.14" terminal_size = "0.1" ansi_term = "0.12" csv = "1.1" +regex = "1.5" [dev-dependencies] pretty_assertions = "0.7.2" diff --git a/src/commands/display.rs b/src/commands/display.rs index 93d13c2..270df3a 100644 --- a/src/commands/display.rs +++ b/src/commands/display.rs @@ -12,15 +12,16 @@ use crate::database::{Database, DBVersion}; use crate::formatters::Formatter; use crate::config::Config; use crate::models::Entry; +use crate::timeparse::parse_time; use super::Command; fn local_to_utc(t: DateTime) -> error::Result> { let local_time = match Local.from_local_datetime(&t.naive_utc()) { - LocalResult::None => return Err(error::Error::NoneLocalTime(t.naive_utc())), + LocalResult::None => return Err(error::Error::NoneLocalTime(t.naive_utc().to_string())), LocalResult::Single(t) => t, LocalResult::Ambiguous(t1, t2) => return Err(error::Error::AmbiguousLocalTime { - orig: t.naive_utc(), + orig: t.naive_utc().to_string(), t1, t2, }), }; @@ -83,8 +84,8 @@ impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { fn try_from(matches: &'a ArgMatches) -> error::Result { Ok(Args { ids: matches.is_present("ids"), - start: matches.value_of("at").map(|s| s.parse()).transpose()?, - end: matches.value_of("at").map(|s| s.parse()).transpose()?, + start: matches.value_of("start").map(|s| parse_time(s)).transpose()?, + end: matches.value_of("end").map(|s| parse_time(s)).transpose()?, format: matches.value_of("format").unwrap().parse()?, grep: matches.value_of("grep").map(|s| s.into()), sheet: matches.value_of("sheet").map(|s| s.parse()).transpose()?, diff --git a/src/commands/in.rs b/src/commands/in.rs index c08203c..4cf28c7 100644 --- a/src/commands/in.rs +++ b/src/commands/in.rs @@ -9,6 +9,7 @@ use crate::error; use crate::editor; use crate::commands::Command; use crate::config::Config; +use crate::timeparse::parse_time; pub struct Args { at: Option>, @@ -20,7 +21,7 @@ impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { fn try_from(matches: &'a ArgMatches) -> Result { Ok(Args { - at: matches.value_of("at").map(|s| s.parse()).transpose()?, + at: matches.value_of("at").map(|s| parse_time(s)).transpose()?, note: matches.value_of("note").map(|s| s.into()), }) } diff --git a/src/error.rs b/src/error.rs index f5d2c5f..f9ca9c5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -27,8 +27,8 @@ pub enum Error { #[error("Couldn't parse yaml file at: {path}\nwith error: {error}")] YamlError{ path: PathBuf, error: serde_yaml::Error}, - #[error("Could not parse datetime: {0}")] - DateTimeParseError(#[from] chrono::format::ParseError), + #[error("Could not understand '{0}' as a date format.")] + DateTimeParseError(String), #[error("IOError: {0}")] IOError(#[from] std::io::Error), @@ -40,11 +40,11 @@ pub enum Error { CorruptedData(String), #[error("Trying to parse {0} as a time in your timezone led to no results")] - NoneLocalTime(NaiveDateTime), + NoneLocalTime(String), #[error("Trying to parse {orig} as a time in your timezone led to the ambiguous results {t1} and {t2}")] AmbiguousLocalTime { - orig: NaiveDateTime, + orig: String, t1: DateTime, t2: DateTime, } diff --git a/src/lib.rs b/src/lib.rs index ef131bf..a4c5460 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod editor; pub mod formatters; pub mod error; pub mod models; +pub mod timeparse; #[cfg(test)] pub mod test_utils; diff --git a/src/timeparse.rs b/src/timeparse.rs new file mode 100644 index 0000000..f5075af --- /dev/null +++ b/src/timeparse.rs @@ -0,0 +1,115 @@ +use chrono::{DateTime, Utc, Local, TimeZone, LocalResult}; +use regex::Regex; + +use crate::error::{Result, Error}; + +pub fn parse_time(input: &str) -> Result> { + // first try to parse as a full datetime with optional timezone + let re = Regex::new(r"(?xi) + (?P\d{4}) # the year, mandatory + . + (?P\d{2}) # the month, mandatory + . + (?P\d{2}) # the day, mandatory + (. # a separator + (?P\d{2}) # the hour, optional + (. # a separator + (?P\d{2})? # the minute, optional + (. # a separator + (?P\d{2}))?)?)? # the second, optional, implies minute + (?P(?PZ)|(?P(\+|-)\d{2}:\d{2}))? # the offset, optional + ").unwrap(); + + if let Some(caps) = re.captures(input) { + if let Some(_) = caps.name("offset") { + // start with a specific offset or utc + if let Some(_) = caps.name("utc") { + // start with utc + unimplemented!() + } else { + // start with an offset + unimplemented!() + } + } else { + // start with a local time + let try_date = Local.ymd_opt( + (&caps["year"]).parse().unwrap(), + (&caps["month"]).parse().unwrap(), + (&caps["day"]).parse().unwrap(), + ).and_hms_opt( + caps.name("hour").map(|m| m.as_str().parse().unwrap()).unwrap_or(0), + caps.name("minute").map(|m| m.as_str().parse().unwrap()).unwrap_or(0), + caps.name("second").map(|m| m.as_str().parse().unwrap()).unwrap_or(0), + ); + + return 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, t2, + }), + }; + } + } + + Err(Error::DateTimeParseError(input.into())) +} + +#[cfg(test)] +mod tests { + use chrono::TimeZone; + + use super::*; + + #[test] + fn parse_datetime_string() { + std::env::set_var("TZ", "America/Mexico_City"); + + assert_eq!(parse_time("2021-05-21 11:36").unwrap(), Utc.ymd(2021, 5, 21).and_hms(16, 36, 0)); + assert_eq!(parse_time("2021-05-21 11:36:12").unwrap(), Utc.ymd(2021, 5, 21).and_hms(16, 36, 12)); + + assert_eq!(parse_time("2021-05-21T11:36").unwrap(), Utc.ymd(2021, 5, 21).and_hms(16, 36, 0)); + assert_eq!(parse_time("2021-05-21T11:36:12").unwrap(), Utc.ymd(2021, 5, 21).and_hms(16, 36, 12)); + } + + #[test] + fn parse_date() { + assert_eq!(parse_time("2021-05-21").unwrap(), Utc.ymd(2021, 5, 21).and_hms(5, 0, 0)); + } + + #[test] + fn parse_hour() { + std::env::set_var("TZ", "America/Mexico_City"); + + let now = Utc::now(); + + assert_eq!(parse_time("11:36").unwrap(), now.date().and_hms(16, 36, 0)); + assert_eq!(parse_time("11:36:35").unwrap(), now.date().and_hms(16, 36, 35)); + } + + #[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)); + } + + #[test] + fn parse_human_time() { + assert_eq!(parse_time("an hour ago").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + assert_eq!(parse_time("two hours ago").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + assert_eq!(parse_time("4 hours ago").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + + assert_eq!(parse_time("a minute ago").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + assert_eq!(parse_time("two minutes ago").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + assert_eq!(parse_time("4 minutes ago").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + + assert_eq!(parse_time("hace una hora").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + assert_eq!(parse_time("hace dos horas").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + assert_eq!(parse_time("hace cuatro horas").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + + assert_eq!(parse_time("hace una minuto").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + assert_eq!(parse_time("hace dos minutos").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + assert_eq!(parse_time("hace cuatro minutos").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12)); + } +}