diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.lock | 173 | ||||
-rw-r--r-- | Cargo.toml | 11 | ||||
-rw-r--r-- | src/lib.rs | 439 | ||||
-rw-r--r-- | src/main.rs | 75 | ||||
-rw-r--r-- | tests/tests.rs | 217 |
6 files changed, 917 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1b2b82a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,173 @@ +[[package]] +name = "aho-corasick" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cfg-if" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.43" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "pidgin" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "regex" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ucd-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "two_timer" +version = "0.1.0" +dependencies = [ + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pidgin 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ucd-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "utf8-ranges" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9a933f4e58658d7b12defcf96dc5c720f20832deebe3e0a19efd3b6aaeeb9e" +"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" +"checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" +"checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1" +"checksum libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)" = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d" +"checksum memchr 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0a3eb002f0535929f1199681417029ebea04aadc0c7a4224b46be99c7f5d6a16" +"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" +"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" +"checksum pidgin 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ebfb7c2a7cadeeac248ecfcf74e514342bede5709780accf6b4225e059745f20" +"checksum redox_syscall 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "679da7508e9a6390aeaf7fbd02a800fdc64b73fe2204dd2c8ae66d22d9d5ad5d" +"checksum regex 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ee84f70c8c08744ea9641a731c7fadb475bf2ecc52d7f627feb833e0b3990467" +"checksum regex-syntax 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fbc557aac2b708fe84121caf261346cc2eed71978024337e42eb46b8a252ac6e" +"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +"checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" +"checksum ucd-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d0f8bfa9ff0cadcd210129ad9d2c5f145c13e9ced3d3e5d948a6213487d52444" +"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737" +"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" +"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e67d97d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "two_timer" +version = "0.1.0" +authors = ["dfhoughton <dfhoughton@gmail.com>"] +edition = "2018" + +[dependencies] +pidgin = "0.2" +lazy_static = "1.2" +chrono = "0.4" +regex = "1"
\ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..437e438 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,439 @@ +#![recursion_limit = "1024"] +#[macro_use] +extern crate pidgin; +#[macro_use] +extern crate lazy_static; +extern crate chrono; +use chrono::offset::LocalResult; +use chrono::{DateTime, Datelike, Duration, TimeZone, Timelike, Utc, Weekday}; +use pidgin::{Match, Matcher}; +use regex::Regex; + +lazy_static! { + static ref GRAMMAR: Matcher = grammar!{ + (?ibBw) + + TOP -> r(r"\A") <something> r(r"\z") + + something => <universal> | <existential> + universal => [["always", "ever", "all time", "forever", "from beginning to end", "from the beginning to the end"]] + existential => <one_moment> | <two_moments> + one_moment => <at_moment> + two_moments -> <at_moment> <to> <at_moment> + to => [["to", "through", "until", "up to", "thru", "till"]] | r("-+") + at_moment -> <at_time>? <moment> <at_time>? | <time> + moment => <specific> | <relative> + specific => <adverb> | <date_with_year> + relative => ("bar") + adverb => <now> | <today> | <tomorrow> | <yesterday> + now => ("now") + today => ("today") + tomorrow => ("tomorrow") + yesterday => ("yesterday") + date_with_year => <n_date> | <a_date> + at_time -> ("at") <time> + time -> <hour_12> <am_pm>? | <hour_24> + hour_24 => <h24> + hour_24 => <h24> (":") <minute> + hour_24 => <h24> (":") <minute> (":") <second> + hour_12 => <h12> + hour_12 => <h12> (":") <minute> + hour_12 => <h12> (":") <minute> (":") <second> + minute => [ (0..60).into_iter().map(|i| format!("'{:02}", i)).collect::<Vec<_>>() ] + second => [ (0..60).into_iter().map(|i| format!("'{:02}", i)).collect::<Vec<_>>() ] + am_pm => (?-i) [["am", "AM", "pm", "PM", "a.m.", "A.M.", "p.m.", "P.M."]] + h12 => [(1..=12).into_iter().collect::<Vec<_>>()] + h24 => [(1..=24).into_iter().collect::<Vec<_>>()] + n_date -> <year> ("/") <n_month> ("/") <n_day> + n_date -> <year> ("-") <n_month> ("-") <n_day> + n_date -> <year> (".") <n_month> (".") <n_day> + n_date -> <year> ("/") <n_day> ("/") <n_month> + n_date -> <year> ("-") <n_day> ("-") <n_month> + n_date -> <year> (".") <n_day> (".") <n_month> + n_date -> <n_month> ("/") <n_day> ("/") <year> + n_date -> <n_month> ("-") <n_day> ("-") <year> + n_date -> <n_month> (".") <n_day> (".") <year> + n_date -> <n_day> ("/") <n_month> ("/") <year> + n_date -> <n_day> ("-") <n_month> ("-") <year> + n_date -> <n_day> (".") <n_month> (".") <year> + a_date -> <a_month> <n_day> (",") <year> + a_date -> <n_day> <a_month> <year> + a_date -> <a_day> (",") <a_month> <n_day> (",") <year> + year => [ + (100..=3000) + .into_iter() + .collect::<Vec<_>>() + ] + year => [ + (0..=99) + .into_iter() + .flat_map(|i| vec![format!("'{:02}", i), format!("{:02}", i)]) + .collect::<Vec<_>>() + ] + n_day => [ + (1..=31) + .into_iter() + .flat_map(|i| vec![i.to_string(), format!("{:02}", i)]) + .collect::<Vec<_>>() + ] + n_month => [ + (1..12) + .into_iter() + .flat_map(|i| vec![format!("{:02}", i), format!("{}", i)]) + .collect::<Vec<_>>() + ] + a_day => [ + "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Tues Weds Thurs Tues. Weds. Thurs." + .split(" ") + .into_iter() + .flat_map(|w| vec![ + w.to_string(), + w[0..2].to_string(), + w[0..3].to_string(), + format!("{}.", w[0..2].to_string()), + format!("{}.", w[0..3].to_string()), + ]) + .collect::<Vec<_>>() + ] + a_day => (?-i) [["M", "T", "W", "R", "F", "S", "U"]] + a_month => [ + "January February March April May June July August September October November December" + .split(" ") + .into_iter() + .flat_map(|w| vec![w.to_string(), w[0..3].to_string()]) + .collect::<Vec<_>>() + ] + }.matcher().unwrap(); +} + +pub fn parse( + phrase: &str, + now: Option<&DateTime<Utc>>, + period: Option<Period>, +) -> Result<(DateTime<Utc>, DateTime<Utc>), String> { + let parse = GRAMMAR.parse(phrase); + // println!("{:?}", GRAMMAR.rx); + // println!("what I got: {:?}", parse); + if parse.is_none() { + return Err(format!( + "could not parse \"{}\" as a time expression", + phrase + )); + } + let parse = parse.unwrap(); + if parse.has("universal") { + return Ok(( + chrono::MIN_DATE.and_hms_milli(0, 0, 0, 0), + chrono::MAX_DATE.and_hms_milli(23, 59, 59, 999), + )); + } + let parse = parse.name("existential").unwrap(); + let now = if now.is_some() { + now.unwrap().clone() + } else { + Utc::now() + }; + let period = if period.is_some() { + period.unwrap() + } else { + Period::Minute + }; + if let Some(moment) = parse.name("one_moment") { + if let Some(specific) = moment.name("specific") { + return specific_moment(specific, &now, &period); + } + if let Some(relative) = moment.name("relative") { + return Ok(relative_moment(relative, &now, &now, true)); + } + unreachable!(); + } + if let Some(two_moments) = parse.name("two_moments") { + let moments = two_moments.all_names("moment"); + let first = moments[0]; + let last = moments[1]; + if first.has("specific") { + if last.has("specific") { + return match specific_moment(first, &now, &period) { + Ok((d1, _)) => match specific_moment(last, &now, &period) { + Ok((_, d2)) => { + if d1 <= d2 { + Ok((d1, d2)) + } else { + Err(format!("{} is after {}", first.as_str(), last.as_str())) + } + } + Err(s) => Err(s), + }, + Err(s) => Err(s), + }; + } else { + return match specific_moment(first, &now, &period) { + Ok((d1, _)) => { + let (_, d2) = relative_moment(last, &now, &d1, false); + Ok((d1, d2)) + } + Err(s) => Err(s), + }; + } + } else if last.has("specific") { + return match specific_moment(last, &now, &period) { + Ok((_, d2)) => { + let (d1, _) = relative_moment(first, &now, &d2, true); + Ok((d1, d2)) + } + Err(s) => Err(s), + }; + } else { + let (_, d2) = relative_moment(last, &now, &now, true); + let (d1, _) = relative_moment(first, &now, &d2, true); + return Ok((d1, d2)); + } + } + unreachable!(); +} + +fn specific_moment( + m: &Match, + now: &DateTime<Utc>, + period: &Period, +) -> Result<(DateTime<Utc>, DateTime<Utc>), String> { + let now = now.clone(); + if let Some(adverb) = m.name("adverb") { + if adverb.has("now") { + return Ok(moment_to_period(now, &period)); + } + if adverb.has("today") { + return Ok(moment_to_period(now, &Period::Day)); + } + if adverb.has("tomorrow") { + return Ok(moment_to_period(now + Duration::days(1), &Period::Day)); + } + if adverb.has("yesterday") { + return Ok(moment_to_period(now - Duration::days(1), &Period::Day)); + } + unreachable!(); + } + if let Some(date) = m.name("date_with_year") { + if let Some(date) = date.name("n_date") { + let year = year(date, &now); + let month = n_month(date); + let day = n_day(date); + let d_opt = Utc.ymd_opt(year, month, day); + return match d_opt { + LocalResult::None => Err(format!( + "cannot construct UTC date with year {}, month {}, and day {}", + year, month, day + )), + LocalResult::Single(d1) => { + let d1 = d1.and_hms(0, 0, 0); + Ok((d1, d1 + Duration::days(1))) + } + LocalResult::Ambiguous(_, _) => Err(format!( + "cannot construct unambiguous UTC date with year {}, month {}, and day {}", + year, month, day + )), + }; + } + if let Some(date) = date.name("a_date") { + let year = year(date, &now); + let month = a_month(date); + let day = n_day(date); + let d_opt = Utc.ymd_opt(year, month, day); + return match d_opt { + LocalResult::None => Err(format!( + "cannot construct UTC date with year {}, month {}, and day {}", + year, month, day + )), + LocalResult::Single(d1) => { + if let Some(wd) = date.name("a_day") { + let wd = weekday(wd.as_str()); + if wd == d1.weekday() { + let d1 = d1.and_hms(0, 0, 0); + Ok((d1, d1 + Duration::days(1))) + } else { + Err(format!( + "the weekday of year {}, month {}, day {} is not {}", + year, + month, + day, + date.name("a_day").unwrap().as_str() + )) + } + } else { + let d1 = d1.and_hms(0, 0, 0); + Ok((d1, d1 + Duration::days(1))) + } + } + LocalResult::Ambiguous(_, _) => Err(format!( + "cannot construct unambiguous UTC date with year {}, month {}, and day {}", + year, month, day + )), + }; + } + unreachable!(); + } + unimplemented!(); +} + +fn a_month(m: &Match) -> u32 { + match m.name("a_month").unwrap().as_str()[0..3] + .to_lowercase() + .as_ref() + { + "jan" => 1, + "feb" => 2, + "mar" => 3, + "apr" => 4, + "may" => 5, + "jun" => 6, + "jul" => 7, + "aug" => 8, + "sep" => 9, + "oct" => 10, + "nov" => 11, + "dec" => 12, + _ => unreachable!(), + } +} + +fn n_month(m: &Match) -> u32 { + lazy_static! { + static ref MONTH: Regex = Regex::new(r"\A0?(\d{1,2})\z").unwrap(); + } + let cap = MONTH.captures(m.name("n_month").unwrap().as_str()).unwrap(); + cap[1].parse::<u32>().unwrap() +} + +fn year(m: &Match, now: &DateTime<Utc>) -> i32 { + lazy_static! { + static ref YEAR: Regex = Regex::new(r"\A(?:'0?|0)?(\d{1,2})\z").unwrap(); + } + let year = m.name("year").unwrap().as_str(); + let cap = YEAR.captures(year); + if let Some(cap) = cap { + // year is assumed to be in the current century + let y = cap[1].parse::<i32>().unwrap(); + let this_year = now.year() % 100; + if this_year < y { + now.year() - this_year - 100 + y + } else { + now.year() - this_year + y + } + } else { + year.parse::<i32>().unwrap() + } +} + +fn n_day(m: &Match) -> u32 { + m.name("n_day").unwrap().as_str().parse::<u32>().unwrap() +} + +/// expand a moment to the period containing it +fn moment_to_period(now: DateTime<Utc>, period: &Period) -> (DateTime<Utc>, DateTime<Utc>) { + match period { + Period::Year => { + let d1 = Utc.ymd(now.year(), 1, 1).and_hms(0, 0, 0); + let d2 = Utc.ymd(now.year() + 1, 1, 1).and_hms(0, 0, 0); + (d1, d2) + } + Period::Month => { + let d1 = Utc.ymd(now.year(), now.month(), 1).and_hms(0, 0, 0); + let d2 = if now.month() == 12 { + Utc.ymd(now.year() + 1, 1, 1) + } else { + Utc.ymd(now.year(), now.month() + 1, 1) + } + .and_hms(0, 0, 0); + (d1, d2) + } + Period::Week => { + let d1 = Utc.ymd(now.year(), now.month(), now.day()).and_hms(0, 0, 0) + - Duration::days(now.weekday().num_days_from_monday() as i64); + (d1, d1 + Duration::days(7)) + } + Period::WeekStartingSunday => { + let d1 = Utc.ymd(now.year(), now.month(), now.day()).and_hms(0, 0, 0) + - Duration::days(now.weekday().num_days_from_sunday() as i64); + (d1, d1 + Duration::days(7)) + } + Period::Day => { + let d1 = Utc.ymd(now.year(), now.month(), now.day()).and_hms(0, 0, 0); + (d1, d1 + Duration::days(1)) + } + Period::Hour => { + let d1 = Utc + .ymd(now.year(), now.month(), now.day()) + .and_hms(now.hour(), 0, 0); + (d1, d1 + Duration::hours(1)) + } + Period::Minute => { + let d1 = + Utc.ymd(now.year(), now.month(), now.day()) + .and_hms(now.hour(), now.minute(), 0); + (d1, d1 + Duration::minutes(1)) + } + Period::Second => { + let d1 = Utc.ymd(now.year(), now.month(), now.day()).and_hms( + now.hour(), + now.minute(), + now.second(), + ); + (d1, d1 + Duration::seconds(1)) + } + Period::Nanosecond => (now, now + Duration::nanoseconds(1)), + } +} + +fn relative_moment( + m: &Match, + now: &DateTime<Utc>, + other_time: &DateTime<Utc>, + before: bool, +) -> (DateTime<Utc>, DateTime<Utc>) { + unimplemented!(); +} + +pub enum Period { + Year, + Month, + Week, + WeekStartingSunday, + Day, + Hour, + Minute, + Second, + Nanosecond, +} + +fn weekday(s: &str) -> Weekday { + match s.chars().nth(0).expect("empty string") { + 'm' | 'M' => Weekday::Mon, + 't' | 'T' => { + if s.len() == 1 { + Weekday::Tue + } else { + match s.chars().nth(1).unwrap() { + 'u' | 'U' => Weekday::Tue, + 'h' | 'H' => Weekday::Thu, + _ => unreachable!(), + } + } + } + 'w' | 'W' => Weekday::Wed, + 'H' => Weekday::Thu, + 'F' | 'f' => Weekday::Fri, + 'S' | 's' => { + if s.len() == 1 { + Weekday::Sat + } else { + match s.chars().nth(1).unwrap() { + 'a' | 'A' => Weekday::Sat, + 'u' | 'U' => Weekday::Sun, + _ => unreachable!(), + } + } + } + 'U' => Weekday::Sun, + _ => unreachable!(), + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0d9bc79 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,75 @@ +#![recursion_limit="512"] +#[macro_use] +extern crate pidgin; + +fn main() { + let g = grammar!{ + (?ibBw) + + TOP => <universal> | <existential> + existential => <specific> | <relative> + specific => ("foo") + relative => ("bar") + universal => [["always", "ever", "all time"]] + existential => <date> | <two_times> + two_times => <two_dates> | <on_date> + two_dates -> <date> <date_separator> <date> + on_date -> [["on"]]? <date> [["from"]] <time> [["to"]] <time> + date_separator => [["-", "through", "to", "until", "till", "til", "thru"]] + date => <specific> | <relative> + month => <a_month> | <n_month> + time -> <hour_12> <am_pm> | <hour_24> + hour_24 => <h24> + hour_24 => <h24> (":") <minute> + hour_24 => <h24> (":") <minute> (":") <second> + hour_12 => <h12> + hour_12 => <h12> (":") <minute> + hour_12 => <h12> (":") <minute> (":") <second> + minute => [ (0..60).into_iter().map(|i| format!("'{:02}", i)).collect::<Vec<_>>() ] + second => [ (0..60).into_iter().map(|i| format!("'{:02}", i)).collect::<Vec<_>>() ] + am_pm => (?-i) [["am", "AM", "pm", "PM", "a.m.", "A.M.", "p.m.", "P.M."]] + h12 => [(1..=12).into_iter().collect::<Vec<_>>()] + h24 => [(1..=24).into_iter().collect::<Vec<_>>()] + day => <a_day> | <n_date> + a_day => [ + "Sunday Monday Tuesday Wednesday Thursday Friday Saturday" + .split(" ") + .into_iter() + .flat_map(|w| vec![w.to_string(), w[0..2].to_string(), w[0..3].to_string()]) + .collect::<Vec<_>>() + ] + a_day => (?-i) [["M", "T", "W", "R", "F", "S", "U"]] + n_date -> <year> ("/") <n_month> ("/") <n_day> + n_date -> <year> ("-") <n_month> ("-") <n_day> + n_date -> <year> (".") <n_month> (".") <n_day> + n_date -> <year> ("/") <n_day> ("/") <n_month> + n_date -> <year> ("-") <n_day> ("-") <n_month> + n_date -> <year> (".") <n_day> (".") <n_month> + year => [ + (1..=3000) + .into_iter() + .collect::<Vec<_>>() + ] + year => [ + (0..=99) + .into_iter() + .flat_map(|i| vec![format!("'{:02}", i), format!("{:02}", i)]) + .collect::<Vec<_>>() + ] + n_day => [ + (1..=31) + .into_iter() + .flat_map(|i| vec![i.to_string(), format!("{:02}", i)]) + .collect::<Vec<_>>() + ] + n_month => [(1..12).into_iter().collect::<Vec<_>>()] + a_month => [ + "January February March April May June July August September October November December" + .split(" ") + .into_iter() + .flat_map(|w| vec![w.to_string(), w[0..3].to_string()]) + .collect::<Vec<_>>() + ] + }; + println!("Hello, world!"); +} diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..e49ec38 --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,217 @@ +#![feature(test)] +extern crate two_timer; +use two_timer::parse; +extern crate chrono; +use chrono::{Duration, TimeZone, Utc}; + +#[test] +fn always() { + let alpha = chrono::MIN_DATE.and_hms_milli(0, 0, 0, 0); + let omega = chrono::MAX_DATE.and_hms_milli(23, 59, 59, 999); + for phrase in [ + "always", + "ever", + "all time", + "forever", + "from beginning to end", + "from the beginning to the end", + ] + .iter() + { + let (start, end) = parse(phrase, None, None).unwrap(); + assert_eq!(alpha, start); + assert_eq!(omega, end); + } +} + +#[test] +fn yesterday() { + let now = Utc::now(); + let (start, end) = parse("yesterday", Some(&now), None).unwrap(); + assert!(start < now); + assert!(end < now); + let then = now - Duration::days(1); + assert!(start < then); + assert!(then < end); + let then = then - Duration::days(1); + assert!(then < start); +} + +#[test] +fn tomorrow() { + let now = Utc::now(); + let (start, end) = parse("tomorrow", Some(&now), None).unwrap(); + assert!(start > now); + assert!(end > now); + let then = now + Duration::days(1); + assert!(start < then); + assert!(then < end); + let then = then + Duration::days(1); + assert!(then > end); +} + +#[test] +fn today() { + let now = Utc::now(); + let (start, end) = parse("today", Some(&now), None).unwrap(); + assert!(start < now); + assert!(end > now); + let then = now + Duration::days(1); + assert!(start < then); + assert!(then > end); + let then = now - Duration::days(1); + assert!(then < start); + assert!(then < end); +} + +#[test] +fn ymd_5_6_69() { + let then = Utc.ymd(1969, 5, 6).and_hms(0, 0, 0); + for phrase in [ + "5-6-69", "5/6/69", "5.6.69", "5/6/1969", "5-6-1969", "5.6.1969", "69-5-6", "69/5/6", + "69.5.6", "1969/5/6", "1969-5-6", "1969.5.6", "5-6-'69", "5/6/'69", "5.6.'69", "'69-5-6", + "'69/5/6", "'69.5.6", + ] + .iter() + { + let (start, end) = parse(phrase, None, None).unwrap(); + assert_eq!(then, start); + assert_eq!(then + Duration::days(1), end); + } +} + +#[test] +fn alphabetic_5_6_69() { + let then = Utc.ymd(1969, 5, 6).and_hms(0, 0, 0); + for phrase in [ + "May 6, 1969", + "May 6, '69", + "May 6, 69", + "6 May 1969", + "6 May '69", + "6 May 69", + "Tuesday, May 6, 1969", + "Tuesday, May 6, '69", + "Tuesday, May 6, 69", + "Tues, May 6, 1969", + "Tues, May 6, '69", + "Tues, May 6, 69", + "Tue, May 6, 1969", + "Tue, May 6, '69", + "Tue, May 6, 69", + "Tu, May 6, 1969", + "Tu, May 6, '69", + "Tu, May 6, 69", + "Tues., May 6, 1969", + "Tues., May 6, '69", + "Tues., May 6, 69", + "Tue., May 6, 1969", + "Tue., May 6, '69", + "Tue., May 6, 69", + "Tu., May 6, 1969", + "Tu., May 6, '69", + "Tu., May 6, 69", + "T, May 6, 1969", + "T, May 6, '69", + "T, May 6, 69", + ] + .iter() + { + let (start, end) = parse(phrase, None, None).unwrap(); + assert_eq!(then, start); + assert_eq!(then + Duration::days(1), end); + } +} + +#[test] +fn ymd_5_31_69() { + let then = Utc.ymd(1969, 5, 31).and_hms(0, 0, 0); + for phrase in [ + "5-31-69", + "5/31/69", + "5.31.69", + "5/31/1969", + "5-31-1969", + "5.31.1969", + "69-5-31", + "69/5/31", + "69.5.31", + "1969/5/31", + "1969-5-31", + "1969.5.31", + "5-31-'69", + "5/31/'69", + "5.31.'69", + "'69-5-31", + "'69/5/31", + "'69.5.31", + "31-5-69", + "31/5/69", + "31.5.69", + "31/5/1969", + "31-5-1969", + "31.5.1969", + "69-31-5", + "69/31/5", + "69.31.5", + "1969/31/5", + "1969-31-5", + "1969.31.5", + "31-5-'69", + "31/5/'69", + "31.5.'69", + "'69-31-5", + "'69/31/5", + "'69.31.5", + "05-31-69", + "05/31/69", + "05.31.69", + "05/31/1969", + "05-31-1969", + "05.31.1969", + "69-05-31", + "69/05/31", + "69.05.31", + "1969/05/31", + "1969-05-31", + "1969.05.31", + "05-31-'69", + "05/31/'69", + "05.31.'69", + "'69-05-31", + "'69/05/31", + "'69.05.31", + "31-05-69", + "31/05/69", + "31.05.69", + "31/05/1969", + "31-05-1969", + "31.05.1969", + "69-31-05", + "69/31/05", + "69.31.05", + "1969/31/05", + "1969-31-05", + "1969.31.05", + "31-05-'69", + "31/05/'69", + "31.05.'69", + "'69-31-05", + "'69/31/05", + "'69.31.05", + ] + .iter() + { + let (start, end) = parse(phrase, None, None).unwrap(); + assert_eq!(then, start); + assert_eq!(then + Duration::days(1), end); + } +} + +#[test] +fn leap_day() { + let rv = parse("2019-02-29", None, None); + assert!(rv.is_err()); + let rv = parse("2020-02-29", None, None); + assert!(rv.is_ok()); +} |