summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordfhoughton <dfhoughton@gmail.com>2020-10-03 16:22:49 -0400
committerdfhoughton <dfhoughton@gmail.com>2020-10-03 16:22:49 -0400
commit8354b38700673fc2f7bbd6e66f950d1ba85e424a (patch)
treee0904cc4401af4d2f8d917f0f438638dd9b4d271
parentbcceeada2dd619fe5bf82b1076dfd4e3071bbf3a (diff)
downloadtwo-timer-8354b38700673fc2f7bbd6e66f950d1ba85e424a.zip
added configuration parameter so time expressions can default to future instead of past interpretation
-rw-r--r--CHANGES.md2
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml2
-rw-r--r--src/lib.rs71
-rw-r--r--tests/tests.rs135
5 files changed, 186 insertions, 26 deletions
diff --git a/CHANGES.md b/CHANGES.md
index 6be1f94..d97cf75 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,7 @@
# Change Log
+## 2.2.0
+* adding `default_to_past` configuration parameter to allow people to interpret relative times like "Tuesday" as the nearest such moment in the future rather than the past
## 2.1.0 *2020-5-17*
* added since expressions: "since yesterday", "since the beginning of the month", "since the end of last year", "after midnight", ...
* added "the" as a synonym of "this" as a period modifier: "the beginning of the month" = "the beginning of this month"
diff --git a/Cargo.lock b/Cargo.lock
index faf1b97..a976a53 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -171,7 +171,7 @@ dependencies = [
[[package]]
name = "two_timer"
-version = "2.1.0"
+version = "2.2.0"
dependencies = [
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/Cargo.toml b/Cargo.toml
index f9aa416..2727b91 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "two_timer"
-version = "2.1.0"
+version = "2.2.0"
authors = ["dfhoughton <dfhoughton@gmail.com>"]
description="parser for English time expressions"
homepage="https://github.com/dfhoughton/two-timer"
diff --git a/src/lib.rs b/src/lib.rs
index fd88824..c0fc1ad 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,13 +4,11 @@ This crate provides a `parse` function to convert English time expressions into
of timestamps representing a time range. It converts "today" into the first and last
moments of today, "May 6, 1968" into the first and last moments of that day, "last year"
into the first and last moments of that year, and so on. It does this even for expressions
-generally interpreted as referring to a point in time, such as "3 PM". In these cases
-the width of the time span varies according to the specificity of the expression. "3 PM" has
-a granularity of an hour, "3:00 PM", of a minute, "3:00:00 PM", of a second. For pointwise
-expression the first moment is the point explicitly named. The `parse` expression actually
-returns a 3-tuple consisting of the two timestamps and whether the expression is literally
-a range -- two time expressions separated by a preposition such as "to", "through", "up to",
-or "until".
+generally interpreted as referring to a point in time, such as "3 PM", though for these it
+always assumes a granularity of one second. For pointwise expression the first moment is the
+point explicitly named. The `parse` expression actually returns a 3-tuple consisting of the
+two timestamps and whether the expression is literally a range -- two time expressions
+separated by a preposition such as "to", "through", "up to", or "until".
# Example
@@ -103,6 +101,8 @@ are
describes a time after the first
3. if neither expression is fully-specified, the first will be interpreted relative to "now" and the
second relative to the first
+4. a moment interpreted relative to "now" will be assumed to be before now unless the configuration
+parameter `default_to_past` is set to `false`, in which case it will be assumed to be after now
The rules of interpretation for relative time expressions in ranges will likely be refined further
in the future.
@@ -117,8 +117,9 @@ more natural interpretation might be "3:00 PM to 4:00 PM".
Since it is common to abbreviate years to the last two digits of the century, two-digit
years will be interpreted as abbreviated unless followed by a suffix such as "B.C.E." or "AD".
-They will be interpreted as the the nearest appropriate *previous* year to the current moment,
-so in 2010 "'11" will be interpreted as 1911, not 2011.
+They will be interpreted by default as the the nearest appropriate *previous* year to the current moment,
+so in 2010 "'11" will be interpreted as 1911, not 2011. If you set the configuration parameter
+`default_to_past` to `false` this is reversed, so "'11" in 2020 will be interpreted as 2111.
# The Second Time in Ranges
@@ -190,7 +191,7 @@ slower than the Perl version. To address this I added an optional feature to two
```toml
[dependencies.two_timer]
-version = "~1.3.0"
+version = "~2.2"
features = ["small_grammar"]
```
@@ -764,8 +765,8 @@ pub fn parse(
Err(s) => Err(s),
};
} else {
- // the first moment is assumed to be before now
- return match relative_moment(first, &config, &config.now, true) {
+ // the first moment is assumed to be before now if default_to_past is true, otherwise it is after
+ return match relative_moment(first, &config, &config.now, config.default_to_past) {
Ok((d1, d2)) => {
let (d1, _) = adjust(d1, d2, first);
// the second moment is necessarily after the first moment
@@ -794,6 +795,7 @@ pub struct Config {
period: Period,
pay_period_length: u32,
pay_period_start: Option<NaiveDate>,
+ default_to_past: bool,
}
impl Config {
@@ -805,6 +807,7 @@ impl Config {
period: Period::Minute,
pay_period_length: 7,
pay_period_start: None,
+ default_to_past: true,
}
}
/// Returns a copy of the configuration parameters with the "now" moment
@@ -845,6 +848,16 @@ impl Config {
c.pay_period_start = pay_period_start;
c
}
+ /// Returns a copy of the configuration parameters with the `default_to_past`
+ /// parameter set as specified. This allows the interpretation of relative time expressions
+ /// like "Friday" and "12:00". By default, these expressions are assumed to refer to the
+ /// most recent such interval in the *past*. By setting `default_to_past` to `false`
+ /// the rule changes so they are assumed to refer to the nearest such interval in the future.
+ pub fn default_to_past(&self, default_to_past: bool) -> Config {
+ let mut c = self.clone();
+ c.default_to_past = default_to_past;
+ c
+ }
}
/// A simple categorization of things that could go wrong.
@@ -921,7 +934,7 @@ fn specific(m: &Match) -> bool {
}
fn n_date(date: &Match, config: &Config) -> Result<NaiveDate, TimeError> {
- let year = year(date, &config.now);
+ let year = year(date, config);
let month = n_month(date);
let day = n_day(date);
match NaiveDate::from_ymd_opt(year, month, day) {
@@ -986,7 +999,7 @@ fn handle_specific_day(
};
}
if let Some(date) = date.name("a_date") {
- let year = year(date, &now);
+ let year = year(date, config);
let month = a_month(date);
let day = if date.has("n_day") {
n_day(date)
@@ -1072,7 +1085,7 @@ fn handle_specific_period(
return Ok(span);
}
if let Some(moment) = moment.name("month_and_year") {
- let y = year(moment, &config.now);
+ let y = year(moment, &config);
let m = a_month(moment);
return match NaiveDate::from_ymd_opt(y, m, 1) {
None => unreachable!(),
@@ -1178,7 +1191,7 @@ fn handle_specific_period(
};
}
if let Some(moment) = moment.name("year") {
- let year = year(moment, &config.now);
+ let year = year(moment, config);
return Ok(moment_to_period(
NaiveDate::from_ymd(year, 1, 1).and_hms(0, 0, 0),
&Period::Year,
@@ -1274,7 +1287,7 @@ fn handle_one_time(
} else if let Some(moment) = moment.name("specific_time") {
handle_specific_time(moment, config)
} else {
- relative_moment(moment, config, &config.now, true)
+ relative_moment(moment, config, &config.now, config.default_to_past)
};
match r {
Ok((d1, d2)) => Ok((d1, d2, false)),
@@ -1307,7 +1320,7 @@ fn relative_moment(
m: &Match,
config: &Config,
other_time: &NaiveDateTime,
- before: bool,
+ before: bool, // whether the time found should be before or after the reference time
) -> Result<(NaiveDateTime, NaiveDateTime), TimeError> {
if let Some(a_month_and_a_day) = m.name("a_day_in_month") {
return match month_and_a_day(a_month_and_a_day, config, other_time, before) {
@@ -1550,15 +1563,27 @@ fn n_month(m: &Match) -> u32 {
cap[1].parse::<u32>().unwrap()
}
-fn year(m: &Match, now: &NaiveDateTime) -> i32 {
+fn year(m: &Match, config: &Config) -> i32 {
let year = m.name("year").unwrap();
if let Some(sy) = year.name("short_year") {
let y = s_to_n(sy.as_str()) as i32;
- let this_year = now.year() % 100;
- if this_year < y {
- now.year() - this_year - 100 + y
+ let this_year = config.now.year() % 100;
+ if config.default_to_past {
+ if this_year < y {
+ // previous century
+ config.now.year() - this_year - 100 + y
+ } else {
+ // this century
+ config.now.year() - this_year + y
+ }
} else {
- now.year() - this_year + y
+ if this_year > y {
+ // next century
+ config.now.year() - this_year + 100 + y
+ } else {
+ // this century
+ config.now.year() - this_year + y
+ }
}
} else if let Some(suffix) = year.name("year_suffix") {
let y = s_to_n(year.name("suffix_year").unwrap().as_str()) as i32;
diff --git a/tests/tests.rs b/tests/tests.rs
index c2eb23a..f33eeb5 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -172,6 +172,18 @@ fn at_3_pm() {
}
#[test]
+fn at_3_pm_default_to_future() {
+ let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(14, 0, 0);
+ let then = NaiveDate::from_ymd(1969, 5, 6).and_hms(15, 0, 0);
+ for phrase in ["3 PM", "3 pm", "15"].iter() {
+ let (start, end, _) =
+ parse(phrase, Some(Config::new().now(now).default_to_past(false))).unwrap();
+ assert_eq!(then, start);
+ assert_eq!(then + Duration::seconds(1), end);
+ }
+}
+
+#[test]
fn at_3_00_pm() {
let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(16, 0, 0);
let then = NaiveDate::from_ymd(1969, 5, 6).and_hms(15, 0, 0);
@@ -352,6 +364,21 @@ fn may_1969() {
}
#[test]
+fn short_year_past_vs_future() {
+ let m1 = NaiveDate::from_ymd(1969, 5, 1).and_hms(0, 0, 0);
+ let m2 = NaiveDate::from_ymd(1969, 6, 1).and_hms(0, 0, 0);
+ let now = NaiveDate::from_ymd(2020, 5, 6).and_hms(0, 0, 0);
+ let (start, end, _) = parse("May '69", Some(Config::new().now(now))).unwrap();
+ assert_eq!(m1, start);
+ assert_eq!(m2, end);
+ let m1 = NaiveDate::from_ymd(2069, 5, 1).and_hms(0, 0, 0);
+ let m2 = NaiveDate::from_ymd(2069, 6, 1).and_hms(0, 0, 0);
+ let (start, end, _) = parse("May '69", Some(Config::new().now(now).default_to_past(false))).unwrap();
+ assert_eq!(m1, start);
+ assert_eq!(m2, end);
+}
+
+#[test]
fn this_month() {
let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(0, 0, 0);
let d1 = NaiveDate::from_ymd(1969, 5, 1).and_hms(0, 0, 0);
@@ -781,6 +808,19 @@ fn monday() {
}
#[test]
+fn monday_default_to_future() {
+ let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(0, 0, 0);
+ let then = NaiveDate::from_ymd(1969, 5, 12).and_hms(0, 0, 0);
+ let (start, end, _) = parse(
+ "Monday",
+ Some(Config::new().now(now).default_to_past(false)),
+ )
+ .unwrap();
+ assert_eq!(then, start);
+ assert_eq!(then + Duration::days(1), end);
+}
+
+#[test]
fn friday_at_3_pm() {
let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(0, 0, 0);
let then = NaiveDate::from_ymd(1969, 5, 2).and_hms(15, 0, 0);
@@ -808,6 +848,19 @@ fn monday_at_3_pm() {
}
#[test]
+fn monday_at_3_pm_default_to_future() {
+ let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(0, 0, 0);
+ let then = NaiveDate::from_ymd(1969, 5, 12).and_hms(15, 0, 0);
+ let (start, end, _) = parse(
+ "Monday at 3 pm",
+ Some(Config::new().now(now).default_to_past(false)),
+ )
+ .unwrap();
+ assert_eq!(then, start);
+ assert_eq!(then + Duration::seconds(1), end);
+}
+
+#[test]
fn just_may() {
let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(0, 0, 0);
let d1 = NaiveDate::from_ymd(1969, 5, 1).and_hms(0, 0, 0);
@@ -838,6 +891,17 @@ fn just_june() {
}
#[test]
+fn just_june_default_to_future() {
+ let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(0, 0, 0);
+ let d1 = NaiveDate::from_ymd(1969, 6, 1).and_hms(0, 0, 0);
+ let d2 = NaiveDate::from_ymd(1969, 7, 1).and_hms(0, 0, 0);
+ let (start, end, _) =
+ parse("June", Some(Config::new().now(now).default_to_past(false))).unwrap();
+ assert_eq!(d1, start);
+ assert_eq!(d2, end);
+}
+
+#[test]
fn monday_through_friday() {
let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(0, 0, 0);
let d1 = NaiveDate::from_ymd(1969, 5, 5).and_hms(0, 0, 0);
@@ -848,6 +912,20 @@ fn monday_through_friday() {
}
#[test]
+fn monday_through_friday_default_to_future() {
+ let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(0, 0, 0);
+ let d1 = NaiveDate::from_ymd(1969, 5, 12).and_hms(0, 0, 0);
+ let d2 = NaiveDate::from_ymd(1969, 5, 17).and_hms(0, 0, 0);
+ let (start, end, _) = parse(
+ "Monday through Friday",
+ Some(Config::new().now(now).default_to_past(false)),
+ )
+ .unwrap();
+ assert_eq!(d1, start);
+ assert_eq!(d2, end);
+}
+
+#[test]
fn tuesday_through_friday() {
let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(0, 0, 0);
let d1 = NaiveDate::from_ymd(1969, 4, 29).and_hms(0, 0, 0);
@@ -1324,6 +1402,32 @@ fn day_and_month() {
}
#[test]
+fn day_and_month_default_to_future() {
+ let now = NaiveDate::from_ymd(1969, 6, 16).and_hms(0, 0, 0);
+ let d1 = NaiveDate::from_ymd(1970, 5, 15).and_hms(0, 0, 0);
+ let d2 = d1 + Duration::days(1);
+ let patterns = [
+ "the ides of May",
+ "5-15",
+ "May fifteenth",
+ "May the 15th",
+ "May the fifteenth",
+ ];
+ for p in patterns.iter() {
+ match parse(p, Some(Config::new().now(now).default_to_past(false))) {
+ Ok((start, end, _)) => {
+ assert_eq!(d1, start);
+ assert_eq!(d2, end);
+ }
+ Err(e) => {
+ println!("{:?}", e);
+ assert!(false, "didn't match");
+ }
+ }
+ }
+}
+
+#[test]
fn one_week_before_may_6_1969() {
let d1 = NaiveDate::from_ymd(1969, 5, 6).and_hms(0, 0, 0) - Duration::days(7);
let patterns = ["one week before May 6, 1969", "1 week before May 6, 1969"];
@@ -1441,7 +1545,36 @@ fn noon() {
let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(0, 0, 0);
match parse("noon on May 6, 1969", Some(Config::new().now(now))) {
Ok((start, end, _)) => {
- assert_eq!(d1, start, "'noon' is the same as 'noon today'");
+ assert_eq!(d1, start);
+ assert_eq!(d2, end);
+ }
+ Err(e) => {
+ println!("{:?}", e);
+ assert!(false, "didn't match");
+ }
+ }
+}
+
+#[test]
+fn simple_noon_past_and_future() {
+ let now = NaiveDate::from_ymd(1969, 5, 6).and_hms(0, 0, 0);
+ let d1 = NaiveDate::from_ymd(1969, 5, 5).and_hms(12, 0, 0);
+ let d2 = d1 + Duration::seconds(1);
+ match parse("noon", Some(Config::new().now(now))) {
+ Ok((start, end, _)) => {
+ assert_eq!(d1, start);
+ assert_eq!(d2, end);
+ }
+ Err(e) => {
+ println!("{:?}", e);
+ assert!(false, "didn't match");
+ }
+ }
+ let d1 = d1 + Duration::days(1);
+ let d2 = d2 + Duration::days(1);
+ match parse("noon", Some(Config::new().now(now).default_to_past(false))) {
+ Ok((start, end, _)) => {
+ assert_eq!(d1, start);
assert_eq!(d2, end);
}
Err(e) => {