summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordfhoughton <dfhoughton@gmail.com>2018-12-23 10:58:27 -0500
committerdfhoughton <dfhoughton@gmail.com>2018-12-23 10:58:27 -0500
commit76a9737079a2d99b18855335d4cd361192def177 (patch)
tree31015927b118ce4994b06760c849c54b74096f79
downloadtwo-timer-76a9737079a2d99b18855335d4cd361192def177.zip
first commit
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock173
-rw-r--r--Cargo.toml11
-rw-r--r--src/lib.rs439
-rw-r--r--src/main.rs75
-rw-r--r--tests/tests.rs217
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());
+}