use crate::dimensions::numeral::helpers::{integer_value, numeral_data};
use crate::dimensions::time_grain::Grain;
use crate::pattern::{dim, predicate, regex};
use crate::types::{DimensionKind, Rule, TokenData};
use super::{Direction, EarlyLate, IntervalDirection, PartOfDay, TimeData, TimeForm};
fn is_integer_between(lo: i64, hi: i64) -> Box<dyn Fn(&TokenData) -> bool + Send + Sync> {
Box::new(move |td: &TokenData| {
if let Some(v) = integer_value(td) {
v >= lo && v <= hi
} else {
false
}
})
}
fn time_data(td: &TokenData) -> Option<&TimeData> {
match td {
TokenData::Time(d) => Some(d),
_ => None,
}
}
fn is_time(td: &TokenData) -> bool {
matches!(td, TokenData::Time(_))
}
fn is_part_of_day(td: &TokenData) -> bool {
matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::PartOfDay(_)))
}
fn is_time_of_day(td: &TokenData) -> bool {
matches!(
td,
TokenData::Time(TimeData {
form: TimeForm::Hour(..) | TimeForm::HourMinute(..) | TimeForm::HourMinuteSecond(..),
..
})
)
}
fn is_day_of_week(td: &TokenData) -> bool {
matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::DayOfWeek(_)))
}
fn is_month_token(td: &TokenData) -> bool {
matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_)))
}
fn is_year_token(td: &TokenData) -> bool {
matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Year(_)))
}
fn is_ok_with_this_next(td: &TokenData) -> bool {
matches!(td, TokenData::Time(d) if d.ok_for_this_next)
}
fn is_not_latent_time(td: &TokenData) -> bool {
matches!(td, TokenData::Time(d) if !d.latent)
}
fn compose(t1: &TimeData, t2: &TimeData) -> TimeData {
let mut td = TimeData::new(TimeForm::Composed(
Box::new(t1.clone()),
Box::new(t2.clone()),
));
td.timezone = t1.timezone.clone().or_else(|| t2.timezone.clone());
td
}
fn get_dom_value(td: &TokenData) -> Option<u32> {
match td {
TokenData::Numeral(n) => {
let v = n.value as u32;
if (1..=31).contains(&v) {
Some(v)
} else {
None
}
}
TokenData::Ordinal(o) => {
let v = o.value as u32;
if (1..=31).contains(&v) {
Some(v)
} else {
None
}
}
_ => None,
}
}
fn intersect_dom(month_td: &TimeData, day: u32) -> TokenData {
let dom_td = TimeData::new(TimeForm::DayOfMonth(day));
let mut composed = TimeData::new(TimeForm::Composed(
Box::new(month_td.clone()),
Box::new(dom_td),
));
composed.direction = month_td.direction;
TokenData::Time(composed)
}
fn month_day(month: u32, day: u32) -> TimeForm {
TimeForm::Composed(
Box::new(TimeData::new(TimeForm::Month(month))),
Box::new(TimeData::new(TimeForm::DayOfMonth(day))),
)
}
fn is_month_day_no_year(td: &TokenData) -> bool {
match td {
TokenData::Time(d) => match &d.form {
TimeForm::DateMDY { year: None, .. } => true,
TimeForm::Composed(a, b) => {
(matches!(a.form, TimeForm::Month(_)) && matches!(b.form, TimeForm::DayOfMonth(_)))
|| (matches!(b.form, TimeForm::Month(_))
&& matches!(a.form, TimeForm::DayOfMonth(_)))
}
_ => false,
},
_ => false,
}
}
fn extract_month_day(td: &TokenData) -> Option<(u32, u32)> {
match td {
TokenData::Time(d) => match &d.form {
TimeForm::DateMDY {
month,
day,
year: None,
} => Some((*month, *day)),
TimeForm::Composed(a, b) => {
if let (TimeForm::Month(m), TimeForm::DayOfMonth(d)) = (&a.form, &b.form) {
Some((*m, *d))
} else if let (TimeForm::DayOfMonth(d), TimeForm::Month(m)) = (&a.form, &b.form) {
Some((*m, *d))
} else {
None
}
}
_ => None,
},
_ => None,
}
}
pub fn rules() -> Vec<Rule> {
vec![
Rule {
name: "this|next <day-of-week>".to_string(),
pattern: vec![regex(
r"\b(mondays?|tuesdays?|wednesdays?|thursdays?|fridays?|saturdays?|sundays?|mon|tue|wed|thu|fri|sat|sun)\b\.?",
)],
production: Box::new(|nodes| {
let text = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let text_lower = text.to_lowercase();
let text_singular = text_lower.trim_end_matches('s');
let dow = match text_singular {
"monday" | "mon" => 0,
"tuesday" | "tue" => 1,
"wednesday" | "wed" => 2,
"thursday" | "thu" => 3,
"friday" | "fri" => 4,
"saturday" | "sat" => 5,
"sunday" | "sun" => 6,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::DayOfWeek(dow))))
}),
},
Rule {
name: "month name".to_string(),
pattern: vec![regex(
r"\b(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|oct|nov|dec)\b\.?",
)],
production: Box::new(|nodes| {
let text = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let lower = text.to_lowercase();
let month = match lower.as_ref() {
"january" | "jan" => 1,
"february" | "feb" => 2,
"march" | "mar" => 3,
"april" | "apr" => 4,
"may" => 5,
"june" | "jun" => 6,
"july" | "jul" => 7,
"august" | "aug" => 8,
"september" | "sep" => 9,
"october" | "oct" => 10,
"november" | "nov" => 11,
"december" | "dec" => 12,
_ => return None,
};
let latent = matches!(lower.as_ref(), "may" | "march");
if latent {
Some(TokenData::Time(TimeData::latent(TimeForm::Month(month))))
} else {
Some(TokenData::Time(TimeData::new(TimeForm::Month(month))))
}
}),
},
Rule {
name: "now".to_string(),
pattern: vec![regex(r"\b(now|((just|right)\s*)now|immediately|at the moment|atm)\b")],
production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Now)))),
},
Rule {
name: "as soon as possible".to_string(),
pattern: vec![regex(r"\b(asap|as soon as possible)\b")],
production: Box::new(|_| {
let mut td = TimeData::new(TimeForm::Now);
td.open_interval_direction = Some(IntervalDirection::After);
Some(TokenData::Time(td))
}),
},
Rule {
name: "today".to_string(),
pattern: vec![regex(r"\btodays?|(at this time)\b")],
production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Today)))),
},
Rule {
name: "tomorrow".to_string(),
pattern: vec![regex(r"\b(tmrw?|tomm?or?rows?)\b")],
production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Tomorrow)))),
},
Rule {
name: "yesterday".to_string(),
pattern: vec![regex(r"\byesterdays?\b")],
production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Yesterday)))),
},
Rule {
name: "day after tomorrow".to_string(),
pattern: vec![regex(r"\b(the )?day after tomorrow\b")],
production: Box::new(|_| {
Some(TokenData::Time(TimeData::new(TimeForm::DayAfterTomorrow)))
}),
},
Rule {
name: "day before yesterday".to_string(),
pattern: vec![regex(r"\b(the )?day before yesterday\b")],
production: Box::new(|_| {
Some(TokenData::Time(TimeData::new(TimeForm::DayBeforeYesterday)))
}),
},
Rule {
name: "noon|midnight|EOD|end of day".to_string(),
pattern: vec![regex(r"\b(noon|(the )?midday|mid day)\b")],
production: Box::new(|_| {
Some(TokenData::Time(TimeData::new(TimeForm::Hour(12, false))))
}),
},
Rule {
name: "midnight".to_string(),
pattern: vec![regex(r"\bmidnight\b")],
production: Box::new(|_| {
Some(TokenData::Time(TimeData::new(TimeForm::Hour(0, false))))
}),
},
Rule {
name: "morning (latent)".to_string(),
pattern: vec![regex(r"\b(early ((in|hours of) the )?morning|morning)\b")],
production: Box::new(|nodes| {
let text = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(0).unwrap_or("").to_string(),
_ => return None,
};
let mut td = TimeData::latent(TimeForm::PartOfDay(PartOfDay::Morning));
if text.contains("early") {
td.early_late = Some(EarlyLate::Early);
}
Some(TokenData::Time(td))
}),
},
Rule {
name: "afternoon (latent)".to_string(),
pattern: vec![regex(r"\bafternoon(ish)?\b")],
production: Box::new(|_| {
Some(TokenData::Time(TimeData::latent(TimeForm::PartOfDay(
PartOfDay::Afternoon,
))))
}),
},
Rule {
name: "evening (latent)".to_string(),
pattern: vec![regex(r"\bevening\b")],
production: Box::new(|_| {
Some(TokenData::Time(TimeData::latent(TimeForm::PartOfDay(
PartOfDay::Evening,
))))
}),
},
Rule {
name: "night (latent)".to_string(),
pattern: vec![regex(r"\bnight\b")],
production: Box::new(|_| {
Some(TokenData::Time(TimeData::latent(TimeForm::PartOfDay(
PartOfDay::Night,
))))
}),
},
Rule {
name: "lunch (latent)".to_string(),
pattern: vec![regex(r"\blunch\b")],
production: Box::new(|_| {
Some(TokenData::Time(TimeData::latent(TimeForm::PartOfDay(
PartOfDay::Lunch,
))))
}),
},
Rule {
name: "tonight".to_string(),
pattern: vec![regex(r"\b(tonight|tonite)\b")],
production: Box::new(|_| {
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::Today)),
Box::new(TimeData::new(TimeForm::PartOfDay(PartOfDay::Evening))),
))))
}),
},
Rule {
name: "this <part-of-day>".to_string(),
pattern: vec![regex(r"\b(this|today)\b"), predicate(is_part_of_day)],
production: Box::new(|nodes| {
let pod = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::Today)),
Box::new(pod.clone()),
))))
}),
},
Rule {
name: "in|during the <part-of-day>".to_string(),
pattern: vec![regex(r"\bin the\b"), predicate(is_part_of_day)],
production: Box::new(|nodes| {
let pod = time_data(&nodes[1].token_data)?;
let mut result = pod.clone();
result.latent = false;
Some(TokenData::Time(result))
}),
},
Rule {
name: "last night".to_string(),
pattern: vec![regex(r"\b(last|yesterday)\b"), predicate(is_part_of_day)],
production: Box::new(|nodes| {
let pod = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::Yesterday)),
Box::new(pod.clone()),
))))
}),
},
Rule {
name: "tomorrow <part-of-day>".to_string(),
pattern: vec![regex(r"\btomorrow\b"), predicate(is_part_of_day)],
production: Box::new(|nodes| {
let pod = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::Tomorrow)),
Box::new(pod.clone()),
))))
}),
},
Rule {
name: "late/early/mid <time>".to_string(),
pattern: vec![regex(r"\b(late|early|mid)\b[\s-]?"), predicate(is_time)],
production: Box::new(|nodes| {
let keyword = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
_ => return None,
};
let t = time_data(&nodes[1].token_data)?;
let mut result = t.clone();
result.latent = false;
result.early_late = Some(match keyword.as_str() {
"late" => EarlyLate::Late,
"mid" => EarlyLate::Mid,
_ => EarlyLate::Early,
});
Some(TokenData::Time(result))
}),
},
Rule {
name: "week-end".to_string(),
pattern: vec![regex(r"\bweek[\s-]?ends?\b")],
production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Weekend)))),
},
Rule {
name: "this past weekend".to_string(),
pattern: vec![regex(r"\bthis past\b"), predicate(is_time)],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
let mut new_t = t.clone();
new_t.direction = Some(Direction::Past);
Some(TokenData::Time(new_t))
}),
},
Rule {
name: "season word".to_string(),
pattern: vec![regex(r"\b(spring|summer|fall|autumn|winter)\b")],
production: Box::new(|nodes| {
let text = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let s = match text.to_lowercase().as_ref() {
"spring" => 0,
"summer" => 1,
"fall" | "autumn" => 2,
"winter" => 3,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::Season(s))))
}),
},
Rule {
name: "last|this|next <season>".to_string(),
pattern: vec![regex(r"\b(this|current|next|last|past|previous)\s+seasons?\b")],
production: Box::new(|nodes| {
let q = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
_ => return None,
};
let mut td = TimeData::new(TimeForm::Season(99)); td.direction = if q == "next" {
Some(Direction::Future)
} else if q == "last" || q == "past" || q == "previous" {
Some(Direction::Past)
} else {
None
};
Some(TokenData::Time(td))
}),
},
Rule {
name: "hh:mm".to_string(),
pattern: vec\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let hour: u32 = m.group(1)?.parse().ok()?;
let minute: u32 = m.group(2)?.parse().ok()?;
if hour < 24 && minute < 60 {
let is_12h = hour != 0 && hour < 12;
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(
hour, minute, is_12h,
))))
} else {
None
}
}),
},
Rule {
name: "hh:mm:ss".to_string(),
pattern: vec[:\.](\d{2})\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let hour: u32 = m.group(1)?.parse().ok()?;
let minute: u32 = m.group(2)?.parse().ok()?;
let second: u32 = m.group(3)?.parse().ok()?;
if hour < 24 && minute < 60 && second < 60 {
Some(TokenData::Time(TimeData::new(TimeForm::HourMinuteSecond(
hour, minute, second,
))))
} else {
None
}
}),
},
Rule {
name: "hhhmm".to_string(),
pattern: vec![regex(r"\b(\d{1,2})h(\d{2})?\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let hour: u32 = m.group(1)?.parse().ok()?;
if hour >= 24 {
return None;
}
match m.group(2) {
Some(min_str) => {
let minute: u32 = min_str.parse().ok()?;
if minute < 60 {
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(
hour, minute, false,
))))
} else {
None
}
}
None => Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(
hour, 0, false,
)))),
}
}),
},
Rule {
name: "hhmm (latent)".to_string(),
pattern: vec![regex(r"\b([01]\d|2[0-3])([0-5]\d)\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let hour: u32 = m.group(1)?.parse().ok()?;
let minute: u32 = m.group(2)?.parse().ok()?;
let full_num = hour.checked_mul(100)?.checked_add(minute)?;
if (1900..=2099).contains(&full_num) {
return None;
}
Some(TokenData::Time(TimeData::latent(TimeForm::HourMinute(
hour, minute, hour < 12,
))))
}),
},
Rule {
name: "time HMM-ish".to_string(),
pattern: vec![regex(
r"\b((?:[01]?\d)|(?:2[0-3]))([0-5]\d)\s?(?:ish|approximately|roughly)\b",
)],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let hour: u32 = m.group(1)?.parse().ok()?;
let minute: u32 = m.group(2)?.parse().ok()?;
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(
hour,
minute,
hour < 12,
))))
}),
},
Rule {
name: "HH:MM:SS ampm".to_string(),
pattern: vec[:\.](\d{2})\s?([ap])\.?(\s?m\.?)?",
)],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let hour: u32 = m.group(1)?.parse().ok()?;
let minute: u32 = m.group(2)?.parse().ok()?;
let second: u32 = m.group(3)?.parse().ok()?;
let ap = m.group(4)?;
if hour > 12 || minute >= 60 || second >= 60 {
return None;
}
let is_pm = ap.to_lowercase().starts_with('p');
let hour = if is_pm && hour < 12 {
hour.checked_add(12)?
} else if !is_pm && hour == 12 {
0
} else {
hour
};
Some(TokenData::Time(TimeData::new(TimeForm::HourMinuteSecond(
hour, minute, second,
))))
}),
},
Rule {
name: "HH:MM ampm".to_string(),
pattern: vec\s?([ap])\.?(\s?m\.?)?")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let hour: u32 = m.group(1)?.parse().ok()?;
let minute: u32 = m.group(2)?.parse().ok()?;
let ap = m.group(3)?;
if hour > 12 || minute >= 60 {
return None;
}
let is_pm = ap.to_lowercase().starts_with('p');
let hour = if is_pm && hour < 12 {
hour.checked_add(12)?
} else if !is_pm && hour == 12 {
0
} else {
hour
};
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(
hour, minute, false,
))))
}),
},
Rule {
name: "H ampm".to_string(),
pattern: vec![regex(r"\b(\d{1,2})\s?([ap])\.?\s?m\.?")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let hour: u32 = m.group(1)?.parse().ok()?;
let ap = m.group(2)?;
if hour > 12 || hour == 0 {
return None;
}
let is_pm = ap.to_lowercase().starts_with('p');
let hour = if is_pm && hour < 12 {
hour.checked_add(12)?
} else if !is_pm && hour == 12 {
0
} else {
hour
};
Some(TokenData::Time(TimeData::new(TimeForm::Hour(hour, false))))
}),
},
Rule {
name: "H a/p (latent)".to_string(),
pattern: vec![regex(r"\b(\d{1,2})([ap])\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let hour: u32 = m.group(1)?.parse().ok()?;
let ap = m.group(2)?;
if hour > 12 || hour == 0 {
return None;
}
let is_pm = ap.to_lowercase().starts_with('p');
let hour = if is_pm && hour < 12 {
hour.checked_add(12)?
} else if !is_pm && hour == 12 {
0
} else {
hour
};
Some(TokenData::Time(TimeData::latent(TimeForm::Hour(
hour, false,
))))
}),
},
Rule {
name: "hhmm (military) am|pm".to_string(),
pattern: vec![regex(r"\b([1-9])([0-5]\d)\s?([ap])\.?(\s?m\.?)?")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let hour: u32 = m.group(1)?.parse().ok()?;
let minute: u32 = m.group(2)?.parse().ok()?;
let ap = m.group(3)?;
if hour > 12 || minute >= 60 {
return None;
}
let is_pm = ap.to_lowercase().starts_with('p');
let hour = if is_pm && hour < 12 {
hour.checked_add(12)?
} else if !is_pm && hour == 12 {
0
} else {
hour
};
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(
hour, minute, false,
))))
}),
},
Rule {
name: "HMM in the <pod>".to_string(),
pattern: vec![regex(
r"\b([1-9])([0-5]\d)\s+in the (morning|afternoon|evening)",
)],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let hour: u32 = m.group(1)?.parse().ok()?;
let minute: u32 = m.group(2)?.parse().ok()?;
let pod = m.group(3)?;
if hour > 12 || minute >= 60 {
return None;
}
let is_pm = matches!(pod.to_lowercase().as_ref(), "afternoon" | "evening");
let hour = if is_pm && hour < 12 { hour.checked_add(12)? } else { hour };
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(
hour, minute, false,
))))
}),
},
Rule {
name: "time-of-day (latent)".to_string(),
pattern: vec![predicate(|td| {
if let Some(data) = numeral_data(td) {
let v = data.value;
(1.0..=12.0).contains(&v) && v == v.floor() && data.ok_for_any_time
} else {
false
}
})],
production: Box::new(|nodes| {
let num = numeral_data(&nodes[0].token_data)?;
let hour = num.value as u32;
Some(TokenData::Time(TimeData::latent(TimeForm::Hour(
hour, true,
))))
}),
},
Rule {
name: "<time-of-day> am|pm".to_string(),
pattern: vec![
dim(DimensionKind::Time),
regex(r"\b(a\.?\s?m\.?|p\.?\s?m\.?)"),
],
production: Box::new(|nodes| {
let ampm = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let is_pm = ampm.to_lowercase().starts_with('p');
let td = time_data(&nodes[0].token_data)?;
apply_ampm(&td.form, is_pm)
}),
},
Rule {
name: "<time> a/p suffix".to_string(),
pattern: vec![
predicate(is_not_latent_time),
regex(r"([ap])\.?(?:\b|\s|$)"),
],
production: Box::new(|nodes| {
let ampm = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let is_pm = ampm.to_lowercase() == "p";
let td = time_data(&nodes[0].token_data)?;
match &td.form {
TimeForm::Hour(_, _) | TimeForm::HourMinute(_, _, _) => {
apply_ampm(&td.form, is_pm)
}
_ => None,
}
}),
},
Rule {
name: "<time> <part-of-day>".to_string(),
pattern: vec![
dim(DimensionKind::Time),
regex(r"\bin (the )?(morning|afternoon|evening)\b"),
],
production: Box::new(|nodes| {
let pod_text = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(2)?,
_ => return None,
};
let is_pm = matches!(pod_text.to_lowercase().as_ref(), "afternoon" | "evening");
let td = time_data(&nodes[0].token_data)?;
match &td.form {
TimeForm::Hour(h, true) => {
let hour = if is_pm && *h < 12 { h.checked_add(12)? } else { *h };
Some(TokenData::Time(TimeData::new(TimeForm::Hour(hour, false))))
}
TimeForm::HourMinute(h, m, _) => {
let hour = if is_pm && *h < 12 { h.checked_add(12)? } else { *h };
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(
hour, *m, false,
))))
}
_ => {
let pod = if is_pm {
PartOfDay::Afternoon
} else {
PartOfDay::Morning
};
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(td.clone()),
Box::new(TimeData::new(TimeForm::PartOfDay(pod))),
))))
}
}
}),
},
Rule {
name: "<time> in the AM/PM".to_string(),
pattern: vec![dim(DimensionKind::Time), regex(r"\bin the (am|pm)\b")],
production: Box::new(|nodes| {
let ampm = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let is_pm = ampm.to_lowercase() == "pm";
let td = time_data(&nodes[0].token_data)?;
apply_ampm(&td.form, is_pm)
}),
},
Rule {
name: "<time-of-day> o'clock".to_string(),
pattern: vec![regex(
r"\b(one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|\d{1,2})\s+o[' ]?clock\b",
)],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let text = m.group(1)?;
let hour: u32 = match text.to_lowercase().as_ref() {
"one" | "1" => 1,
"two" | "2" => 2,
"three" | "3" => 3,
"four" | "4" => 4,
"five" | "5" => 5,
"six" | "6" => 6,
"seven" | "7" => 7,
"eight" | "8" => 8,
"nine" | "9" => 9,
"ten" | "10" => 10,
"eleven" | "11" => 11,
"twelve" | "12" => 12,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::Hour(hour, true))))
}),
},
Rule {
name: "at <time-of-day>".to_string(),
pattern: vec![regex(r"(\bat\b|@)"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
let mut result = t.clone();
result.latent = false;
Some(TokenData::Time(result))
}),
},
Rule {
name: "on <day>".to_string(),
pattern: vec![regex(r"\b(on|during)\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
let mut result = t.clone();
result.latent = false;
Some(TokenData::Time(result))
}),
},
Rule {
name: "on a <named-day>".to_string(),
pattern: vec![regex(r"\bon a\b"), predicate(is_day_of_week)],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
let mut result = t.clone();
result.latent = false;
Some(TokenData::Time(result))
}),
},
Rule {
name: "in|during <named-month>|year".to_string(),
pattern: vec![regex(r"\b(in|during)\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
match &t.form {
TimeForm::Month(_)
| TimeForm::Year(_)
| TimeForm::Season(_)
| TimeForm::Holiday(..)
| TimeForm::Quarter(_)
| TimeForm::QuarterYear(_, _) => {
let mut result = t.clone();
result.latent = false;
Some(TokenData::Time(result))
}
_ => None,
}
}),
},
Rule {
name: "around <time>".to_string(),
pattern: vec![
regex(r"\b(around|about|approximately|roughly)\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
let mut result = t.clone();
result.latent = false;
Some(TokenData::Time(result))
}),
},
Rule {
name: "<time> approximately".to_string(),
pattern: vec![
dim(DimensionKind::Time),
regex(r"\b(approximately|roughly|ish)\b"),
],
production: Box::new(|nodes| {
let t = time_data(&nodes[0].token_data)?;
Some(TokenData::Time(t.clone()))
}),
},
Rule {
name: "in|within|after <duration>".to_string(),
pattern: vec![regex(r"\bin\b"), dim(DimensionKind::Duration)],
production: Box::new(|nodes| {
let dur = match &nodes[1].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::RelativeGrain {
n: dur.value,
grain: dur.grain,
})))
}),
},
Rule {
name: "in <number> (implicit minutes)".to_string(),
pattern: vec![regex(r"\bin\b"), predicate(is_integer_between(0, 60))],
production: Box::new(|nodes| {
let n = numeral_data(&nodes[1].token_data)?.value as i64;
Some(TokenData::Time(TimeData::new(TimeForm::RelativeGrain {
n,
grain: Grain::Minute,
})))
}),
},
Rule {
name: "<duration> hence|ago".to_string(),
pattern: vec![dim(DimensionKind::Duration), regex(r"\b(hence|ago)\b")],
production: Box::new(|nodes| {
let dur = match &nodes[0].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
let sign: i64 = match &nodes[1].token_data {
TokenData::RegexMatch(m) => {
if m.group(1)?.eq_ignore_ascii_case("ago") {
-1
} else {
1
}
}
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::RelativeGrain {
n: sign.checked_mul(dur.value)?,
grain: dur.grain,
})))
}),
},
Rule {
name: "<duration> from right now".to_string(),
pattern: vec![dim(DimensionKind::Duration), regex(r"\bfrom right now\b")],
production: Box::new(|nodes| {
let dur = match &nodes[0].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::RelativeGrain {
n: dur.value,
grain: dur.grain,
})),
Box::new(TimeData::new(TimeForm::Now)),
))))
}),
},
Rule {
name: "<duration> from today".to_string(),
pattern: vec![dim(DimensionKind::Duration), regex(r"\bfrom today\b")],
production: Box::new(|nodes| {
let dur = match &nodes[0].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::Today)),
Box::new(TimeData::new(TimeForm::RelativeGrain {
n: dur.value,
grain: dur.grain,
})),
))))
}),
},
Rule {
name: "<duration> from now".to_string(),
pattern: vec![dim(DimensionKind::Duration), regex(r"\bfrom now\b")],
production: Box::new(|nodes| {
let dur = match &nodes[0].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::RelativeGrain {
n: dur.value,
grain: dur.grain,
})))
}),
},
Rule {
name: "<duration> back".to_string(),
pattern: vec![dim(DimensionKind::Duration), regex(r"\bback\b")],
production: Box::new(|nodes| {
let dur = match &nodes[0].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::RelativeGrain {
n: dur.value.checked_neg()?,
grain: dur.grain,
})))
}),
},
Rule {
name: "after <duration>".to_string(),
pattern: vec![regex(r"\bafter\b"), dim(DimensionKind::Duration)],
production: Box::new(|nodes| {
let dur = match &nodes[1].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
let mut td = TimeData::new(TimeForm::RelativeGrain {
n: dur.value,
grain: dur.grain,
});
td.open_interval_direction = Some(IntervalDirection::After);
Some(TokenData::Time(td))
}),
},
Rule {
name: "within <duration>".to_string(),
pattern: vec![regex(r"\bwithin\b"), dim(DimensionKind::Duration)],
production: Box::new(|nodes| {
let dur = match &nodes[1].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
let from = TimeData::new(TimeForm::Now);
let to = TimeData::new(TimeForm::RelativeGrain {
n: dur.value,
grain: dur.grain,
});
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(from),
Box::new(to),
false,
))))
}),
},
Rule {
name: "date MM/DD(/YYYY)".to_string(),
pattern: vec![regex(
r"\b(\d{1,2})\s?[/\-]\s?(\d{1,2})(?:\s?[/\-]\s?(\d{2,4}))?\b",
)],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let v1: u32 = m.group(1)?.parse().ok()?;
let v2: u32 = m.group(2)?.parse().ok()?;
let year = m.group(3).and_then(|y| {
let yr: i32 = y.parse().ok()?;
if yr < 100 {
if yr < 50 {
Some(yr.checked_add(2000)?)
} else {
Some(yr.checked_add(1900)?)
}
} else {
Some(yr)
}
});
if (1..=12).contains(&v1) && (1..=31).contains(&v2) {
Some(TokenData::Time(TimeData::new(TimeForm::DateMDY {
month: v1,
day: v2,
year,
})))
} else {
None
}
}),
},
Rule {
name: "date DD/MM(/YY)".to_string(),
pattern: vec![regex(
r"\b(\d{1,2})\s?[/\-]\s?(\d{1,2})(?:\s?[/\-]\s?(\d{2,4}))?\b",
)],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let day: u32 = m.group(1)?.parse().ok()?;
let month: u32 = m.group(2)?.parse().ok()?;
let year = m.group(3).and_then(|y| {
let yr: i32 = y.parse().ok()?;
if yr < 100 {
if yr < 50 {
Some(yr.checked_add(2000)?)
} else {
Some(yr.checked_add(1900)?)
}
} else {
Some(yr)
}
});
if (13..=31).contains(&day) && (1..=12).contains(&month) {
Some(TokenData::Time(TimeData::new(TimeForm::DateMDY {
month,
day,
year,
})))
} else {
None
}
}),
},
Rule {
name: "date MM DD YYYY".to_string(),
pattern: vec![regex(r"\b(\d{1,2})\s+(\d{1,2})\s+(\d{2,4})\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let month: u32 = m.group(1)?.parse().ok()?;
let day: u32 = m.group(2)?.parse().ok()?;
let mut year: i32 = m.group(3)?.parse().ok()?;
if year < 100 {
year = if year < 50 { year.checked_add(2000)? } else { year.checked_add(1900)? };
}
if (1..=12).contains(&month) && (1..=31).contains(&day) {
Some(TokenData::Time(TimeData::new(TimeForm::DateMDY {
month,
day,
year: Some(year),
})))
} else {
None
}
}),
},
Rule {
name: "date DD MM YYYY".to_string(),
pattern: vec![regex(r"\b(\d{1,2})\s+(\d{1,2})\s+(\d{2,4})\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let day: u32 = m.group(1)?.parse().ok()?;
let month: u32 = m.group(2)?.parse().ok()?;
let mut year: i32 = m.group(3)?.parse().ok()?;
if year < 100 {
year = if year < 50 { year.checked_add(2000)? } else { year.checked_add(1900)? };
}
if (13..=31).contains(&day) && (1..=12).contains(&month) {
Some(TokenData::Time(TimeData::new(TimeForm::DateMDY {
month,
day,
year: Some(year),
})))
} else {
None
}
}),
},
Rule {
name: "mm/yyyy".to_string(),
pattern: vec![regex(r"\b(\d{1,2})\s?/\s?(\d{4})\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let month: u32 = m.group(1)?.parse().ok()?;
let year: i32 = m.group(2)?.parse().ok()?;
if (1..=12).contains(&month) {
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::Month(month))),
Box::new(TimeData::new(TimeForm::Year(year))),
))))
} else {
None
}
}),
},
Rule {
name: "iso8601 datetime with T separator (en extension)".to_string(),
pattern: vec![regex(
r"\b(\d{4})-(0?[1-9]|1[0-2])-(3[01]|[12]\d|0?[1-9])[Tt]([01]?\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?(?:\.\d+)?([Zz])\b",
)],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let year: i32 = m.group(1)?.parse().ok()?;
let month: u32 = m.group(2)?.parse().ok()?;
let day: u32 = m.group(3)?.parse().ok()?;
let hour: u32 = m.group(4)?.parse().ok()?;
let minute: u32 = m.group(5)?.parse().ok()?;
let second: u32 = match m.group(6) {
Some(s) => s.parse().ok()?,
None => 0,
};
if !(1..=12).contains(&month)
|| !(1..=31).contains(&day)
|| hour > 23
|| minute > 59
|| second > 59
{
return None;
}
let date = TimeData::new(TimeForm::DateMDY {
month,
day,
year: Some(year),
});
let tod = if m.group(6).is_some() {
TimeData::new(TimeForm::HourMinuteSecond(hour, minute, second))
} else {
TimeData::new(TimeForm::HourMinute(hour, minute, false))
};
let mut composed =
TimeData::new(TimeForm::Composed(Box::new(date), Box::new(tod)));
composed.timezone = Some("UTC".to_string());
Some(TokenData::Time(composed))
}),
},
Rule {
name: "yyyy-mm-dd".to_string(),
pattern: vec![regex(
r"\b(\d{4})\s?[/\-]\s?(\d{1,2})(?:\s?[/\-]\s?(\d{1,2}))?\b",
)],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let year: i32 = m.group(1)?.parse().ok()?;
let month: u32 = m.group(2)?.parse().ok()?;
if !(1..=12).contains(&month) {
return None;
}
match m.group(3) {
Some(d) => {
let day: u32 = d.parse().ok()?;
if (1..=31).contains(&day) {
Some(TokenData::Time(TimeData::new(TimeForm::DateMDY {
month,
day,
year: Some(year),
})))
} else {
None
}
}
None => {
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::Month(month))),
Box::new(TimeData::new(TimeForm::Year(year))),
))))
}
}
}),
},
Rule {
name: "yyyy-mm".to_string(),
pattern: vec![regex(r"\b(\d{4})\s?[/\-]\s?(\d{1,2})\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let year: i32 = m.group(1)?.parse().ok()?;
let month: u32 = m.group(2)?.parse().ok()?;
if !(1..=12).contains(&month) {
return None;
}
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::Month(month))),
Box::new(TimeData::new(TimeForm::Year(year))),
))))
}),
},
Rule {
name: "date DD.MM.YYYY".to_string(),
pattern: vec![regex(r"\b(\d{1,2})\.(\d{1,2})\.(\d{4})\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let v1: u32 = m.group(1)?.parse().ok()?;
let v2: u32 = m.group(2)?.parse().ok()?;
let year: i32 = m.group(3)?.parse().ok()?;
if (1..=12).contains(&v1) && (1..=31).contains(&v2) {
Some(TokenData::Time(TimeData::new(TimeForm::DateMDY {
month: v1,
day: v2,
year: Some(year),
})))
} else if (13..=31).contains(&v1) && (1..=12).contains(&v2) {
Some(TokenData::Time(TimeData::new(TimeForm::DateMDY {
month: v2,
day: v1,
year: Some(year),
})))
} else {
None
}
}),
},
Rule {
name: "date DD/Mon/YYYY".to_string(),
pattern: vec![regex(
r"\b(\d{1,2})\s?[/\-]\s?(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s?[/\-]\s?(\d{2,4})\b",
)],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let day: u32 = m.group(1)?.parse().ok()?;
let month_name = m.group(2)?;
let year_str = m.group(3)?;
let month = month_name_to_num(month_name)?;
let year: i32 = year_str.parse().ok()?;
let year = if year < 30 {
year.checked_add(2000)?
} else if year < 100 {
year.checked_add(1900)?
} else {
year
};
if (1..=31).contains(&day) {
Some(TokenData::Time(TimeData::new(TimeForm::DateMDY {
month,
day,
year: Some(year),
})))
} else {
None
}
}),
},
Rule {
name: "year (latent)".to_string(),
pattern: vec![regex(r"\b(1\d{3}|20\d{2})\b")],
production: Box::new(|nodes| {
let text = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let year: i32 = text.parse().ok()?;
Some(TokenData::Time(TimeData::latent(TimeForm::Year(year))))
}),
},
Rule {
name: "spelled-out year (numeral)".to_string(),
pattern: vec![predicate(is_integer_between(1900, 2100))],
production: Box::new(|nodes| {
let num = numeral_data(&nodes[0].token_data)?;
let year = num.value as i32;
Some(TokenData::Time(TimeData::latent(TimeForm::Year(year))))
}),
},
Rule {
name: "in <year>".to_string(),
pattern: vec![
regex(r"\bin\b"),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Year(_))),
),
],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
let mut result = t.clone();
result.latent = false;
Some(TokenData::Time(result))
}),
},
Rule {
name: "<year> (bc|ad)".to_string(),
pattern: vec![dim(DimensionKind::Time), regex(r"\b(a\.?d\.?|b\.?c\.?)\b")],
production: Box::new(|nodes| {
let t = time_data(&nodes[0].token_data)?;
match &t.form {
TimeForm::Year(y) => {
let suffix = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(1).unwrap_or("").to_lowercase(),
_ => String::new(),
};
let year = if suffix.contains('b') { y.abs().checked_neg()? } else { *y };
let mut result = TimeData::new(TimeForm::Year(year));
result.latent = false;
Some(TokenData::Time(result))
}
_ => None,
}
}),
},
Rule {
name: "in <integer> AD".to_string(),
pattern: vec![
regex(r"\bin\b"),
predicate(is_integer_between(1, 9999)),
regex(r"\b(a\.?d\.?|b\.?c\.?)\b"),
],
production: Box::new(|nodes| {
let num = numeral_data(&nodes[1].token_data)?;
let year = num.value as i32;
let suffix = match &nodes[2].token_data {
TokenData::RegexMatch(m) => m.group(1).unwrap_or("").to_lowercase(),
_ => String::new(),
};
let year = if suffix.contains('b') { year.checked_neg()? } else { year };
Some(TokenData::Time(TimeData::new(TimeForm::Year(year))))
}),
},
Rule {
name: "<month> <year>".to_string(),
pattern: vec![
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Year(_))),
),
],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[0].token_data)?;
let t2 = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(t1.clone()),
Box::new(t2.clone()),
))))
}),
},
Rule {
name: "last <time>".to_string(),
pattern: vec![
regex(r"\b(this past|last|past|previous)\b"),
predicate(is_ok_with_this_next),
],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
let mut new_t = t.clone();
new_t.direction = Some(Direction::Past);
new_t.latent = false;
Some(TokenData::Time(new_t))
}),
},
Rule {
name: "next <time>".to_string(),
pattern: vec![
regex(r"\b(next|following|upcoming|coming)\b"),
predicate(is_ok_with_this_next),
],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
let mut new_t = t.clone();
new_t.direction = Some(Direction::Future);
new_t.latent = false;
new_t.not_immediate = true;
Some(TokenData::Time(new_t))
}),
},
Rule {
name: "this <time>".to_string(),
pattern: vec![
regex(r"\b(this|current|coming)\b"),
predicate(is_ok_with_this_next),
],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
let mut new_t = t.clone();
new_t.latent = false;
new_t.not_immediate = true;
Some(TokenData::Time(new_t))
}),
},
Rule {
name: "<time> before last|after next".to_string(),
pattern: vec![
predicate(is_ok_with_this_next),
regex(r"\bafter next\b"),
],
production: Box::new(|nodes| {
let t = time_data(&nodes[0].token_data)?;
let mut new_t = t.clone();
new_t.direction = Some(Direction::FarFuture);
new_t.latent = false;
Some(TokenData::Time(new_t))
}),
},
Rule {
name: "first|second|third|fourth|fifth <day-of-week> of <time>".to_string(),
pattern: vec![
dim(DimensionKind::Ordinal),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::DayOfWeek(_))),
),
regex(r"\bof|in\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let n = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let dow = match &nodes[1].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::DayOfWeek(d) => *d,
_ => return None,
},
_ => return None,
};
let base = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthDOWOfTime {
n,
dow,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "last <day-of-week> of <time>".to_string(),
pattern: vec![
regex(r"\blast\b"),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::DayOfWeek(_))),
),
regex(r"\bof|in\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let dow = match &nodes[1].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::DayOfWeek(d) => *d,
_ => return None,
},
_ => return None,
};
let base = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::LastDOWOfTime {
dow,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "last <cycle> of <time>".to_string(),
pattern: vec![
regex(r"\blast\b"),
dim(DimensionKind::TimeGrain),
regex(r"\bof|in\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let grain = match &nodes[1].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
let base = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::LastCycleOfTime {
grain,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "the <ordinal> last <cycle> of <time>".to_string(),
pattern: vec![
regex(r"\b(the\s+)?"),
dim(DimensionKind::Ordinal),
regex(r"\blast\b"),
dim(DimensionKind::TimeGrain),
regex(r"\bof|in|from\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let n = match &nodes[1].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let grain = match &nodes[3].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
let base = time_data(&nodes[5].token_data)?;
Some(TokenData::Time(TimeData::new(
TimeForm::NthLastCycleOfTime {
n,
grain,
base: Box::new(base.clone()),
},
)))
}),
},
Rule {
name: "last weekend of <named-month>".to_string(),
pattern: vec![
regex(r"\blast\s+(week[\s-]?end|wkend)\s+(of|in)\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let base = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::LastDOWOfTime {
dow: 5, base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "<integer> <day-of-week> from <time>".to_string(),
pattern: vec![
predicate(|td| matches!(td, TokenData::Numeral(_))),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::DayOfWeek(_))),
),
regex(r"\bfrom\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let n = numeral_data(&nodes[0].token_data)?.value as i32;
let dow = match &nodes[1].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::DayOfWeek(d) => *d,
_ => return None,
},
_ => return None,
};
let base = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NDOWsFromTime {
n,
dow,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "<integer> <named-day> ago|back".to_string(),
pattern: vec![
predicate(|td| matches!(td, TokenData::Numeral(_))),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::DayOfWeek(_))),
),
regex(r"\b(ago|back)\b"),
],
production: Box::new(|nodes| {
let n = numeral_data(&nodes[0].token_data)?.value as i32;
let dow = match &nodes[1].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::DayOfWeek(d) => *d,
_ => return None,
},
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::NDOWsFromTime {
n: n.checked_neg()?,
dow,
base: Box::new(TimeData::new(TimeForm::Now)),
})))
}),
},
Rule {
name: "nth <time> after <time>".to_string(),
pattern: vec![
dim(DimensionKind::Ordinal),
dim(DimensionKind::Time),
regex(r"\bafter\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let n = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let t1 = time_data(&nodes[1].token_data)?;
let dow = match &t1.form {
TimeForm::DayOfWeek(d) => *d,
_ => return None,
};
let base = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NDOWsFromTime {
n,
dow,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "the closest <day> to <time>".to_string(),
pattern: vec![
regex(r"\b(the\s+)?closest\b"),
dim(DimensionKind::Time),
regex(r"\bto\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let td1 = time_data(&nodes[1].token_data)?;
let td2 = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthClosestToTime {
n: 1,
target: Box::new(td1.clone()),
base: Box::new(td2.clone()),
})))
}),
},
Rule {
name: "the <ordinal> closest <day> to <time>".to_string(),
pattern: vec![
regex(r"\b(the\s+)?"),
dim(DimensionKind::Ordinal),
regex(r"\bclosest\b"),
dim(DimensionKind::Time),
regex(r"\bto\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let n = match &nodes[1].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let td1 = time_data(&nodes[3].token_data)?;
let td2 = time_data(&nodes[5].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthClosestToTime {
n,
target: Box::new(td1.clone()),
base: Box::new(td2.clone()),
})))
}),
},
Rule {
name: "the <ordinal> <cycle> of <time>".to_string(),
pattern: vec![
dim(DimensionKind::Ordinal),
dim(DimensionKind::TimeGrain),
regex(r"\bof|in\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let n = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let grain = match &nodes[1].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
let base = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthGrainOfTime {
n,
grain,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "last day of <time>".to_string(),
pattern: vec![
regex(r"\b(last day|last day) of\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let base = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthLastDayOfTime {
n: 1,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "(the) <ordinal> last day of <time>".to_string(),
pattern: vec![
regex(r"\b(the\s+)?"),
dim(DimensionKind::Ordinal),
regex(r"\blast day of\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let n = match &nodes[1].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let base = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthLastDayOfTime {
n,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "<ordinal> day of <time>".to_string(),
pattern: vec![
dim(DimensionKind::Ordinal),
regex(r"\bday of\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let n = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let base = time_data(&nodes[2].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthGrainOfTime {
n,
grain: Grain::Day,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "this|last|next <cycle>".to_string(),
pattern: vec![
regex(r"\b(this|current|coming|next|the( following)?|last|past|previous|upcoming)\b"),
regex(r"\b(week|month|year|yr|quarter|qtr)\b"),
],
production: Box::new(|nodes| {
let modifier = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
_ => return None,
};
let grain_text = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let grain = text_to_grain(grain_text)?;
let offset = match modifier.as_str() {
"this" | "current" | "the" => 0,
"last" | "past" | "previous" => -1,
"next" | "following" | "upcoming" | "coming" => 1,
_ if modifier.starts_with("the following") => 1,
_ => 0,
};
Some(TokenData::Time(TimeData::new(TimeForm::GrainOffset { grain, offset })))
}),
},
Rule {
name: "the week of <time>".to_string(),
pattern: vec![regex(r"\b(the )?week of\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let base = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthGrainOfTime {
n: 1,
grain: Grain::Week,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "the <cycle> of the <time grain>".to_string(),
pattern: vec![
regex(r"\bthe\b"),
regex(r"\b(week|month|year|yr|quarter|qtr)\b"),
],
production: Box::new(|nodes| {
let grain_text = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let grain = text_to_grain(grain_text)?;
Some(TokenData::Time(TimeData::new(TimeForm::GrainOffset {
grain,
offset: 0,
})))
}),
},
Rule {
name: "last <integer> <grain>".to_string(),
pattern: vec![
regex(r"\b(last|past)\b"),
predicate(is_integer_between(1, 9999)),
dim(DimensionKind::TimeGrain),
],
production: Box::new(|nodes| {
let num = numeral_data(&nodes[1].token_data)?;
let grain = match &nodes[2].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::NthGrain {
n: num.value as i64,
grain,
past: true,
interval: true,
})))
}),
},
Rule {
name: "next <integer> <grain>".to_string(),
pattern: vec![
regex(r"\b(next)\b"),
predicate(is_integer_between(1, 9999)),
dim(DimensionKind::TimeGrain),
],
production: Box::new(|nodes| {
let num = numeral_data(&nodes[1].token_data)?;
let grain = match &nodes[2].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::NthGrain {
n: num.value as i64,
grain,
past: false,
interval: true,
})))
}),
},
Rule {
name: "upcoming <integer> <cycle>".to_string(),
pattern: vec![
regex(r"\b(upcoming)\b"),
predicate(is_integer_between(1, 9999)),
dim(DimensionKind::TimeGrain),
],
production: Box::new(|nodes| {
let num = numeral_data(&nodes[1].token_data)?;
let grain = match &nodes[2].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::NthGrain {
n: num.value as i64,
grain,
past: false,
interval: false,
})))
}),
},
Rule {
name: "<integer> upcoming <grain>".to_string(),
pattern: vec![
predicate(is_integer_between(1, 9999)),
regex(r"\b(upcoming)\b"),
dim(DimensionKind::TimeGrain),
],
production: Box::new(|nodes| {
let num = numeral_data(&nodes[0].token_data)?;
let grain = match &nodes[2].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::NthGrain {
n: num.value as i64,
grain,
past: false,
interval: false,
})))
}),
},
Rule {
name: "the <day-of-month> (ordinal or number) of <named-month>".to_string(),
pattern: vec![
dim(DimensionKind::Ordinal),
regex(r"\b(of )?\b"),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
],
production: Box::new(|nodes| {
let ord = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as u32,
_ => return None,
};
let month_td = time_data(&nodes[2].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
if (1..=31).contains(&ord) {
Some(intersect_dom(month_td, ord))
} else {
None
}
}),
},
Rule {
name: "<named-month>|<named-day> <day-of-month> (ordinal)".to_string(),
pattern: vec![
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
dim(DimensionKind::Ordinal),
],
production: Box::new(|nodes| {
let month_td = time_data(&nodes[0].token_data)?;
let day = match &nodes[1].token_data {
TokenData::Ordinal(d) => d.value as u32,
_ => return None,
};
if (1..=31).contains(&day) {
Some(intersect_dom(month_td, day))
} else {
None
}
}),
},
Rule {
name: "<named-month> <day-of-month> (non ordinal)".to_string(),
pattern: vec![
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
predicate(is_integer_between(1, 31)),
],
production: Box::new(|nodes| {
let month_td = time_data(&nodes[0].token_data)?;
let day = numeral_data(&nodes[1].token_data)?.value as u32;
if (1..=31).contains(&day) {
Some(intersect_dom(month_td, day))
} else {
None
}
}),
},
Rule {
name: "<dow> <month> <day>".to_string(),
pattern: vec![
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::DayOfWeek(_))),
),
regex(r",?\s*"),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
predicate(|td| matches!(td, TokenData::Numeral(_) | TokenData::Ordinal(_))),
],
production: Box::new(|nodes| {
let dow = match &nodes[0].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::DayOfWeek(d) => *d,
_ => return None,
},
_ => return None,
};
let month_td = time_data(&nodes[2].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
let day = get_dom_value(&nodes[3].token_data)?;
let date_td = match intersect_dom(month_td, day) {
TokenData::Time(td) => td,
_ => return None,
};
let dow_td = TimeData::new(TimeForm::DayOfWeek(dow));
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(dow_td),
Box::new(date_td),
))))
}),
},
Rule {
name: "the <day-of-month> (number)".to_string(),
pattern: vec![
predicate(is_integer_between(1, 31)),
regex(r"\b(of )\b"),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
],
production: Box::new(|nodes| {
let day = numeral_data(&nodes[0].token_data)?.value as u32;
let month_td = time_data(&nodes[2].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
Some(intersect_dom(month_td, day))
}),
},
Rule {
name: "<integer> <month>".to_string(),
pattern: vec![
predicate(is_integer_between(1, 31)),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
],
production: Box::new(|nodes| {
let day = numeral_data(&nodes[0].token_data)?.value as u32;
let month_td = time_data(&nodes[1].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
Some(intersect_dom(month_td, day))
}),
},
Rule {
name: "<ordinal> <month>".to_string(),
pattern: vec![
dim(DimensionKind::Ordinal),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
],
production: Box::new(|nodes| {
let day = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as u32,
_ => return None,
};
let month_td = time_data(&nodes[1].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
if (1..=31).contains(&day) {
Some(intersect_dom(month_td, day))
} else {
None
}
}),
},
Rule {
name: "<ordinal> <month> <short-year>".to_string(),
pattern: vec![
predicate(|td| matches!(td, TokenData::Ordinal(_) | TokenData::Numeral(_))),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
predicate(|td| {
if let TokenData::Numeral(n) = td {
n.value >= 0.0 && n.value < 100.0
} else {
false
}
}),
],
production: Box::new(|nodes| {
let day = get_dom_value(&nodes[0].token_data)?;
let month = match &nodes[1].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::Month(m) => *m,
_ => return None,
},
_ => return None,
};
let y = match &nodes[2].token_data {
TokenData::Numeral(n) => n.value as i32,
_ => return None,
};
let year = if y < 30 {
y.checked_add(2000)?
} else if y < 100 {
y.checked_add(1900)?
} else {
y
};
Some(TokenData::Time(TimeData::new(TimeForm::DateMDY {
month,
day,
year: Some(year),
})))
}),
},
Rule {
name: "the ides of <named-month>".to_string(),
pattern: vec![
regex(r"\b(the )?ides of\b"),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
],
production: Box::new(|nodes| {
let month_td = time_data(&nodes[1].token_data)?;
let month = match &month_td.form {
TimeForm::Month(m) => *m,
_ => return None,
};
let day = if month == 3 || month == 5 || month == 7 || month == 10 {
15
} else {
13
};
Some(intersect_dom(month_td, day))
}),
},
Rule {
name: "<day-of-month> (ordinal)".to_string(),
pattern: vec![dim(DimensionKind::Ordinal)],
production: Box::new(|nodes| {
let day = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as u32,
_ => return None,
};
if (1..=31).contains(&day) {
Some(TokenData::Time(TimeData::latent(TimeForm::DayOfMonth(day))))
} else {
None
}
}),
},
Rule {
name: "<numeral> of <grain-offset-month>".to_string(),
pattern: vec![
predicate(|td| {
if let TokenData::Numeral(n) = td {
let v = n.value as u32;
(1..=31).contains(&v) && n.value == (v as f64)
} else {
false
}
}),
regex(r"\b(of|day of)( the)?\b"),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::GrainOffset { grain: Grain::Month, .. })),
),
],
production: Box::new(|nodes| {
let day = match &nodes[0].token_data {
TokenData::Numeral(n) => n.value as u32,
_ => return None,
};
let t = time_data(&nodes[2].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::DayOfMonth(day))),
Box::new(t.clone()),
))))
}),
},
Rule {
name: "the <day-of-month> (ordinal)".to_string(),
pattern: vec![regex(r"\b(the|on the)\b"), dim(DimensionKind::Ordinal)],
production: Box::new(|nodes| {
let day = match &nodes[1].token_data {
TokenData::Ordinal(d) => d.value as u32,
_ => return None,
};
if (1..=31).contains(&day) {
Some(TokenData::Time(TimeData::new(TimeForm::DayOfMonth(day))))
} else {
None
}
}),
},
Rule {
name: "on the <ordinal-word>".to_string(),
pattern: vec![regex(r"\bon (the )?\b"), dim(DimensionKind::Ordinal)],
production: Box::new(|nodes| {
let day = match &nodes[1].token_data {
TokenData::Ordinal(d) => d.value as u32,
_ => return None,
};
if (1..=31).contains(&day) {
Some(TokenData::Time(TimeData::new(TimeForm::DayOfMonth(day))))
} else {
None
}
}),
},
Rule {
name: "<date> <year>".to_string(),
pattern: vec![
predicate(is_month_day_no_year),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Year(_))),
),
],
production: Box::new(|nodes| {
let (month, day) = extract_month_day(&nodes[0].token_data)?;
let year = match &nodes[1].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::Year(y) => *y,
_ => return None,
},
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::DateMDY {
month,
day,
year: Some(year),
})))
}),
},
Rule {
name: "<ordinal> quarter".to_string(),
pattern: vec![dim(DimensionKind::Ordinal), regex(r"\b(quarter|qtr)\b")],
production: Box::new(|nodes| {
let q = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as u32,
_ => return None,
};
if (1..=4).contains(&q) {
Some(TokenData::Time(TimeData::new(TimeForm::Quarter(q))))
} else {
None
}
}),
},
Rule {
name: "the <ordinal> quarter".to_string(),
pattern: vec![
regex(r"\bthe\b"),
dim(DimensionKind::Ordinal),
regex(r"\b(quarter|qtr)\b"),
],
production: Box::new(|nodes| {
let q = match &nodes[1].token_data {
TokenData::Ordinal(d) => d.value as u32,
_ => return None,
};
if (1..=4).contains(&q) {
Some(TokenData::Time(TimeData::new(TimeForm::Quarter(q))))
} else {
None
}
}),
},
Rule {
name: "<ordinal> quarter <year>".to_string(),
pattern: vec![
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Quarter(_))),
),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Year(_))),
),
],
production: Box::new(|nodes| {
let q = match &nodes[0].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::Quarter(q) => *q,
_ => return None,
},
_ => return None,
};
let year = match &nodes[1].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::Year(y) => *y,
_ => return None,
},
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::QuarterYear(
q, year,
))))
}),
},
Rule {
name: "<quarter> of <year>".to_string(),
pattern: vec![
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Quarter(_))),
),
regex(r"\bof\b"),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Year(_))),
),
],
production: Box::new(|nodes| {
let q = match &nodes[0].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::Quarter(q) => *q,
_ => return None,
},
_ => return None,
};
let year = match &nodes[2].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::Year(y) => *y,
_ => return None,
},
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::QuarterYear(
q, year,
))))
}),
},
Rule {
name: "yyyyqq".to_string(),
pattern: vec![regex(r"\b(\d{2,4})q([1-4])\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let year_str = m.group(1)?;
let q: u32 = m.group(2)?.parse().ok()?;
let year: i32 = year_str.parse().ok()?;
let year = if year < 100 { year.checked_add(2000)? } else { year };
Some(TokenData::Time(TimeData::new(TimeForm::QuarterYear(
q, year,
))))
}),
},
Rule {
name: "week".to_string(),
pattern: vec![regex(r"\ball (week|month|year)\b")],
production: Box::new(|nodes| {
let grain_text = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let grain = text_to_grain(grain_text)?;
Some(TokenData::Time(TimeData::new(TimeForm::AllGrain(grain))))
}),
},
Rule {
name: "rest of the week".to_string(),
pattern: vec![regex(r"\brest of (the )?(week|month|year)\b")],
production: Box::new(|nodes| {
let grain_text = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(2)?,
_ => return None,
};
let grain = text_to_grain(grain_text)?;
Some(TokenData::Time(TimeData::new(TimeForm::RestOfGrain(grain))))
}),
},
Rule {
name: "beginning of <grain>".to_string(),
pattern: vec![
regex(r"\b(beginning|start) of( the| this| current)?\b"),
regex(r"\b(week|month|year)\b"),
],
production: Box::new(|nodes| {
let grain_text = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let grain = text_to_grain(grain_text)?;
Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin: true,
target: Box::new(TimeForm::GrainOffset { grain, offset: 0 }),
})))
}),
},
Rule {
name: "end of <grain>".to_string(),
pattern: vec![
regex(r"\b(end) of( the| this| current)?\b"),
regex(r"\b(week|month|year)\b"),
],
production: Box::new(|nodes| {
let grain_text = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let grain = text_to_grain(grain_text)?;
Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin: false,
target: Box::new(TimeForm::GrainOffset { grain, offset: 0 }),
})))
}),
},
Rule {
name: "beginning of <modifier> <grain>".to_string(),
pattern: vec![
regex(r"\b(at the )?(beginning|start) of( the| around)?\b"),
regex(r"\b(next|last|past|previous|coming|following)\b"),
regex(r"\b(week|month|year)\b"),
],
production: Box::new(|nodes| {
let modifier = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let grain_text = match &nodes[2].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let grain = text_to_grain(grain_text)?;
let offset = modifier_to_offset(modifier);
Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin: true,
target: Box::new(TimeForm::GrainOffset { grain, offset }),
})))
}),
},
Rule {
name: "end of <modifier> <grain>".to_string(),
pattern: vec![
regex(r"\b(at the )?(end) of( the| around)?\b"),
regex(r"\b(next|last|past|previous|coming|following)\b"),
regex(r"\b(week|month|year)\b"),
],
production: Box::new(|nodes| {
let modifier = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let grain_text = match &nodes[2].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let grain = text_to_grain(grain_text)?;
let offset = modifier_to_offset(modifier);
Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin: false,
target: Box::new(TimeForm::GrainOffset { grain, offset }),
})))
}),
},
Rule {
name: "beginning of <time>".to_string(),
pattern: vec![
regex(r"\b(beginning|start|end) of( the| this)?\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let begin_text = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let begin = begin_text.to_lowercase() != "end";
let t = time_data(&nodes[1].token_data)?;
match &t.form {
TimeForm::Month(_) | TimeForm::Year(_) => {
Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin,
target: Box::new(t.form.clone()),
})))
}
_ => None,
}
}),
},
Rule {
name: "EOM/BOM/EOY/BOY/EOD".to_string(),
pattern: vec![regex(r"\b(by (the )?|(at )?the )?(eom|bom|eoy|boy|eod)\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let prefix = m.group(0)?.to_lowercase();
let has_by = prefix.starts_with("by");
let abbr = m.group(4)?.to_lowercase();
if has_by {
let grain = match abbr.as_str() {
"eom" | "bom" => Grain::Month,
"eoy" | "boy" => Grain::Year,
"eod" => Grain::Day,
_ => return None,
};
let end_form = TimeForm::BeginEnd {
begin: false,
target: Box::new(TimeForm::GrainOffset { grain, offset: 0 }),
};
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(TimeData::new(TimeForm::Now)),
Box::new(TimeData::new(end_form)),
false,
))))
} else {
match abbr.as_str() {
"eom" => Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin: false,
target: Box::new(TimeForm::GrainOffset {
grain: Grain::Month,
offset: 0,
}),
}))),
"bom" => Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin: true,
target: Box::new(TimeForm::GrainOffset {
grain: Grain::Month,
offset: 0,
}),
}))),
"eoy" => Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin: false,
target: Box::new(TimeForm::GrainOffset {
grain: Grain::Year,
offset: 0,
}),
}))),
"boy" => Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin: true,
target: Box::new(TimeForm::GrainOffset {
grain: Grain::Year,
offset: 0,
}),
}))),
"eod" => Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin: false,
target: Box::new(TimeForm::GrainOffset {
grain: Grain::Day,
offset: 0,
}),
}))),
_ => None,
}
}
}),
},
Rule {
name: "end/beginning of the day/month/year".to_string(),
pattern: vec![regex(r"\b(beginning|end) of (the )?(day|month|year)\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let begin = m.group(1)?.to_lowercase() != "end";
let grain_text = m.group(3)?;
let grain = text_to_grain(grain_text)?;
Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin,
target: Box::new(TimeForm::GrainOffset { grain, offset: 0 }),
})))
}),
},
Rule {
name: "the beginning/end of the day/year".to_string(),
pattern: vec![regex(r"\bthe (beginning|end) of (the )?(day|month|year)\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let begin = m.group(1)?.to_lowercase() != "end";
let grain_text = m.group(3)?;
let grain = text_to_grain(grain_text)?;
Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin,
target: Box::new(TimeForm::GrainOffset { grain, offset: 0 }),
})))
}),
},
Rule {
name: "by <time>".to_string(),
pattern: vec![regex(r"\bby( the)?\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(TimeData::new(TimeForm::Now)),
Box::new(t.clone()),
false,
))))
}),
},
Rule {
name: "until <time>".to_string(),
pattern: vec![
regex(
r"\b(until|through|before|since|after|from|anytime after|sometimes before)\b",
),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let keyword = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
_ => return None,
};
let t = time_data(&nodes[1].token_data)?;
if t.latent && !matches!(t.form, TimeForm::Year(_)) {
return None;
}
let mut result = t.clone();
result.latent = false;
match keyword.as_str() {
"after" | "since" | "from" | "anytime after" => {
result.open_interval_direction = Some(IntervalDirection::After);
}
"before" | "until" | "through" | "sometimes before" => {
result.open_interval_direction = Some(IntervalDirection::Before);
}
_ => {}
}
Some(TokenData::Time(result))
}),
},
Rule {
name: "from|since|after <time>".to_string(),
pattern: vec![regex(r"\b(from|since|after|anytime after)\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
if t.latent && !matches!(t.form, TimeForm::Year(_)) {
return None;
}
let mut result = t.clone();
result.latent = false;
result.open_interval_direction = Some(IntervalDirection::After);
Some(TokenData::Time(result))
}),
},
Rule {
name: "after <part-of-day>".to_string(),
pattern: vec![regex(r"\b(after|before)\b"), predicate(is_part_of_day)],
production: Box::new(|nodes| {
let keyword = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
_ => return None,
};
let pod = time_data(&nodes[1].token_data)?;
let mut result = pod.clone();
result.latent = false;
if keyword == "after" {
result.open_interval_direction = Some(IntervalDirection::After);
} else {
result.open_interval_direction = Some(IntervalDirection::Before);
}
Some(TokenData::Time(result))
}),
},
Rule {
name: "after lunch/work/school".to_string(),
pattern: vec![regex(r"\bafter[\s-]?(lunch|work|school)\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let keyword = m.group(1)?.to_lowercase();
let (start_h, end_h) = match keyword.as_str() {
"lunch" => (13, 17),
"work" => (17, 21),
"school" => (15, 21),
_ => return None,
};
let from = TimeData::new(TimeForm::Hour(start_h, false));
let to = TimeData::new(TimeForm::Hour(end_h, false));
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(from),
Box::new(to),
true, ))))
}),
},
Rule {
name: "holidays".to_string(),
pattern: vec![regex(&holidays_regex())],
production: Box::new(|nodes| {
let text = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(0)?,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::Holiday(
text.to_lowercase(),
None,
))))
}),
},
Rule {
name: "<holiday> <year>".to_string(),
pattern: vec![
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Holiday(..))),
),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Year(_))),
),
],
production: Box::new(|nodes| {
let holiday_name = match &nodes[0].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::Holiday(name, _) => name.clone(),
_ => return None,
},
_ => return None,
};
let year = match &nodes[1].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::Year(y) => *y,
_ => return None,
},
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::Holiday(
holiday_name,
Some(year),
))))
}),
},
Rule {
name: "<holiday> in <year>".to_string(),
pattern: vec![
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Holiday(..))),
),
regex(r"\bin\b"),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Year(_))),
),
],
production: Box::new(|nodes| {
let holiday_name = match &nodes[0].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::Holiday(name, _) => name.clone(),
_ => return None,
},
_ => return None,
};
let year = match &nodes[2].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::Year(y) => *y,
_ => return None,
},
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::Holiday(
holiday_name,
Some(year),
))))
}),
},
Rule {
name: "<duration> after <time>".to_string(),
pattern: vec![
dim(DimensionKind::Duration),
regex(r"\b(after|from)\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let dur = match &nodes[0].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
let t = time_data(&nodes[2].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::DurationAfter {
n: dur.value,
grain: dur.grain,
base: Box::new(t.clone()),
})))
}),
},
Rule {
name: "<latent-time> <time> compose".to_string(),
pattern: vec![
predicate(
|td| matches!(td, TokenData::Time(d) if d.latent && matches!(d.form, TimeForm::Hour(_, true) | TimeForm::HourMinute(_, _, _))),
),
predicate(is_not_latent_time),
],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[0].token_data)?;
let t2 = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(compose(t1, t2)))
}),
},
Rule {
name: "intersect".to_string(),
pattern: vec![dim(DimensionKind::Time), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[0].token_data)?;
let t2 = time_data(&nodes[1].token_data)?;
if std::mem::discriminant(&t1.form) == std::mem::discriminant(&t2.form) {
return None;
}
if t1.latent && t2.latent {
return None;
}
let month_plus_latent_hour = (matches!(t1.form, TimeForm::Month(_))
&& t2.latent
&& matches!(t2.form, TimeForm::Hour(_, _)))
|| (matches!(t2.form, TimeForm::Month(_))
&& t1.latent
&& matches!(t1.form, TimeForm::Hour(_, _)));
if month_plus_latent_hour {
return None;
}
Some(TokenData::Time(compose(t1, t2)))
}),
},
Rule {
name: "intersect by \",\", \"of\", \"from\", \"'s\"".to_string(),
pattern: vec![
dim(DimensionKind::Time),
regex(r"\b(of|from|for)\b( the)?|'s( the)?|,"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[0].token_data)?;
let t2 = time_data(&nodes[2].token_data)?;
if t1.latent && t2.latent {
return None;
}
Some(TokenData::Time(compose(t1, t2)))
}),
},
Rule {
name: "<datetime> - <datetime> (interval)".to_string(),
pattern: vec![
predicate(is_not_latent_time),
regex(r"\s*[\-\u{2013}]\s*"),
predicate(is_not_latent_time),
],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[0].token_data)?;
let t2 = time_data(&nodes[2].token_data)?;
fn is_md_no_year(f: &TimeForm) -> bool {
matches!(f, TimeForm::DateMDY { year: None, .. })
|| matches!(f, TimeForm::Composed(a, b)
if (matches!(a.form, TimeForm::Month(_)) && matches!(b.form, TimeForm::DayOfMonth(_)))
|| (matches!(b.form, TimeForm::Month(_)) && matches!(a.form, TimeForm::DayOfMonth(_))))
}
let year_date_fragment = (matches!(t1.form, TimeForm::Year(_))
&& is_md_no_year(&t2.form))
|| (matches!(t2.form, TimeForm::Year(_))
&& is_md_no_year(&t1.form));
if year_date_fragment {
return None;
}
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(t1.clone()),
Box::new(t2.clone()),
false,
))))
}),
},
Rule {
name: "<time-of-day> - <time-of-day> (interval)".to_string(),
pattern: vec![
predicate(|td| {
matches!(td, TokenData::Time(d) if !d.latent && matches!(d.form,
TimeForm::Hour(_, _) | TimeForm::HourMinute(_, _, _) | TimeForm::HourMinuteSecond(_, _, _)))
}),
regex(r"\s*[\-\u{2013}]\s*|:|\bto\b|\bth?ru\b|\bthrough\b|\b(un)?til(l)?\b"),
predicate(|td| {
matches!(td, TokenData::Time(d) if matches!(d.form,
TimeForm::Hour(_, _) | TimeForm::HourMinute(_, _, _) | TimeForm::HourMinuteSecond(_, _, _)))
}),
],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[0].token_data)?;
let t2 = time_data(&nodes[2].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(t1.clone()),
Box::new(t2.clone()),
false,
))))
}),
},
Rule {
name: "hh(:mm) - <time-of-day> am|pm".to_string(),
pattern: vec![
regex(r"(?:from )?((?:[01]?\d)|(?:2[0-3]))(?:[:.]([0-5]\d))?"),
regex(r"\s*[\-\u{2013}]\s*|\bto\b|\bth?ru\b|\bthrough\b|\b(un)?til(l)?\b"),
predicate(|td| {
matches!(td, TokenData::Time(d) if matches!(d.form,
TimeForm::Hour(_, _) | TimeForm::HourMinute(_, _, _) | TimeForm::HourMinuteSecond(_, _, _)))
}),
regex(r"(?:in the )?([ap])(?:\s|\.)?m?\.?"),
],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let h: u32 = m.group(1)?.parse().ok()?;
let t2 = time_data(&nodes[2].token_data)?;
let ampm = match &nodes[3].token_data {
TokenData::RegexMatch(m) => m.group(1)?,
_ => return None,
};
let is_pm = ampm.to_lowercase() == "p";
let td1_form = match m.group(2) {
Some(mm) => {
let min: u32 = mm.parse().ok()?;
TimeForm::HourMinute(h, min, true)
}
None => TimeForm::Hour(h, true),
};
let td1_applied = apply_ampm(&td1_form, is_pm)?;
let td2_applied = apply_ampm(&t2.form, is_pm)?;
let td1_time = time_data(&td1_applied)?;
let td2_time = time_data(&td2_applied)?;
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(td1_time.clone()),
Box::new(td2_time.clone()),
false,
))))
}),
},
Rule {
name: "dd-dd <month> (interval)".to_string(),
pattern: vec![
predicate(|td| matches!(td, TokenData::Numeral(_) | TokenData::Ordinal(_))),
regex(r"\-|to( the)?|th?ru|through|(un)?til(l)?"),
predicate(|td| matches!(td, TokenData::Numeral(_) | TokenData::Ordinal(_))),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
],
production: Box::new(|nodes| {
let d1 = get_dom_value(&nodes[0].token_data)?;
let d2 = get_dom_value(&nodes[2].token_data)?;
let month_td = time_data(&nodes[3].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
let from = match intersect_dom(month_td, d1) {
TokenData::Time(td) => td,
_ => return None,
};
let to = match intersect_dom(month_td, d2) {
TokenData::Time(td) => td,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(from),
Box::new(to),
false,
))))
}),
},
Rule {
name: "<month> dd-dd (interval)".to_string(),
pattern: vec![
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
predicate(|td| matches!(td, TokenData::Numeral(_) | TokenData::Ordinal(_))),
regex(r"\-|to( the)?|th?ru|through|(un)?til(l)?"),
predicate(|td| matches!(td, TokenData::Numeral(_) | TokenData::Ordinal(_))),
],
production: Box::new(|nodes| {
let month_td = time_data(&nodes[0].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
let d1 = get_dom_value(&nodes[1].token_data)?;
let d2 = get_dom_value(&nodes[3].token_data)?;
let from = match intersect_dom(month_td, d1) {
TokenData::Time(td) => td,
_ => return None,
};
let to = match intersect_dom(month_td, d2) {
TokenData::Time(td) => td,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(from),
Box::new(to),
false,
))))
}),
},
Rule {
name: "from the <day-of-month> (ordinal or number) to the <day-of-month> (ordinal or number) <named-month> (interval)".to_string(),
pattern: vec![
regex(r"\bfrom( the)?\b"),
predicate(|td| matches!(td, TokenData::Numeral(_) | TokenData::Ordinal(_))),
regex(r"\-|to( the)?|th?ru|through|(un)?til(l)?"),
predicate(|td| matches!(td, TokenData::Numeral(_) | TokenData::Ordinal(_))),
regex(r"\b(of )?\b"),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
],
production: Box::new(|nodes| {
let d1 = get_dom_value(&nodes[1].token_data)?;
let d2 = get_dom_value(&nodes[3].token_data)?;
let month_td = time_data(&nodes[5].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
let from = match intersect_dom(month_td, d1) {
TokenData::Time(td) => td,
_ => return None,
};
let to = match intersect_dom(month_td, d2) {
TokenData::Time(td) => td,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(from),
Box::new(to),
false,
))))
}),
},
Rule {
name: "from <month> dd-dd (interval)".to_string(),
pattern: vec![
regex(r"\bfrom\b"),
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_))),
),
predicate(|td| matches!(td, TokenData::Numeral(_) | TokenData::Ordinal(_))),
regex(r"\-|to( the)?|th?ru|through|(un)?til(l)?"),
predicate(|td| matches!(td, TokenData::Numeral(_) | TokenData::Ordinal(_))),
],
production: Box::new(|nodes| {
let month_td = time_data(&nodes[1].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
let d1 = get_dom_value(&nodes[2].token_data)?;
let d2 = get_dom_value(&nodes[4].token_data)?;
let from = match intersect_dom(month_td, d1) {
TokenData::Time(td) => td,
_ => return None,
};
let to = match intersect_dom(month_td, d2) {
TokenData::Time(td) => td,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(from),
Box::new(to),
false,
))))
}),
},
Rule {
name: "later than <tod> before <tod>".to_string(),
pattern: vec![
regex(r"\b(later than|from|(in[\s\-])?between)\b"),
dim(DimensionKind::Time),
regex(r"((but )?before)|\-|to|th?ru|through|(un)?til(l)?"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[1].token_data)?;
let t2 = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(t1.clone()),
Box::new(t2.clone()),
false,
))))
}),
},
Rule {
name: "from <datetime> - <datetime> (interval)".to_string(),
pattern: vec![
regex(r"\b(from|between)\b"),
dim(DimensionKind::Time),
regex(r"\-|to|till|until|and|thru|through"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[1].token_data)?;
let t2 = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(t1.clone()),
Box::new(t2.clone()),
false,
))))
}),
},
Rule {
name: "between <time> and <time>".to_string(),
pattern: vec![
dim(DimensionKind::Time),
regex(r"\b(to|till|until|thru|through)\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[0].token_data)?;
let t2 = time_data(&nodes[2].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(t1.clone()),
Box::new(t2.clone()),
false,
))))
}),
},
Rule {
name: "from <time> for <duration>".to_string(),
pattern: vec![
predicate(is_not_latent_time),
regex(r"\bfor\b"),
dim(DimensionKind::Duration),
],
production: Box::new(|nodes| {
let t = time_data(&nodes[0].token_data)?;
let dur = match &nodes[2].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
let end = TimeData::new(TimeForm::DurationAfter {
n: dur.value,
grain: dur.grain,
base: Box::new(t.clone()),
});
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(t.clone()),
Box::new(end),
false,
))))
}),
},
Rule {
name: "for <duration> from <time>".to_string(),
pattern: vec![
regex(r"\bfor\b"),
dim(DimensionKind::Duration),
regex(r"\b(from|starting|starting from|beginning|after)\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let dur = match &nodes[1].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
let t = time_data(&nodes[3].token_data)?;
let end = TimeData::new(TimeForm::DurationAfter {
n: dur.value,
grain: dur.grain,
base: Box::new(t.clone()),
});
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(t.clone()),
Box::new(end),
false,
))))
}),
},
Rule {
name: "<time> for <duration>".to_string(),
pattern: vec![
regex(r"\b(from|starting|starting from|beginning|after)\b"),
dim(DimensionKind::Time),
regex(r"\bfor\b"),
dim(DimensionKind::Duration),
],
production: Box::new(|nodes| {
let dur = match &nodes[3].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
let t = time_data(&nodes[1].token_data)?;
let end = TimeData::new(TimeForm::DurationAfter {
n: dur.value,
grain: dur.grain,
base: Box::new(t.clone()),
});
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(t.clone()),
Box::new(end),
false,
))))
}),
},
Rule {
name: "half after|past <hour-of-day>".to_string(),
pattern: vec![regex(r"\bhalf (past|after)?\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
match &t.form {
TimeForm::Hour(h, is_12h) => Some(TokenData::Time(TimeData::new(
TimeForm::HourMinute(*h, 30, *is_12h),
))),
_ => None,
}
}),
},
Rule {
name: "quarter after|past <hour-of-day>".to_string(),
pattern: vec![
regex(r"\b(a )?quarter (past|after)\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
match &t.form {
TimeForm::Hour(h, is_12h) => Some(TokenData::Time(TimeData::new(
TimeForm::HourMinute(*h, 15, *is_12h),
))),
_ => None,
}
}),
},
Rule {
name: "quarter to|till|before <hour-of-day>".to_string(),
pattern: vec![
regex(r"\b(a )?quarter (to|before|of)\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
match &t.form {
TimeForm::Hour(h, is_12h) => {
let hour = if *h == 0 { 23 } else { h.checked_sub(1)? };
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(
hour, 45, *is_12h,
))))
}
_ => None,
}
}),
},
Rule {
name: "integer after|past <hour-of-day>".to_string(),
pattern: vec![
predicate(is_integer_between(1, 59)),
regex(r"\b(minutes? )?(past|after|from)\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let mins = numeral_data(&nodes[0].token_data)?.value as u32;
let t = time_data(&nodes[2].token_data)?;
match &t.form {
TimeForm::Hour(h, is_12h) => Some(TokenData::Time(TimeData::new(
TimeForm::HourMinute(*h, mins, *is_12h),
))),
TimeForm::HourMinute(h, 0, is_12h) => Some(TokenData::Time(TimeData::new(
TimeForm::HourMinute(*h, mins, *is_12h),
))),
_ => None,
}
}),
},
Rule {
name: "<integer> to <time>".to_string(),
pattern: vec![
predicate(is_integer_between(1, 59)),
regex(r"\b(minutes? )?(to|before|of|til)\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let mins = numeral_data(&nodes[0].token_data)?.value as u32;
let t = time_data(&nodes[2].token_data)?;
match &t.form {
TimeForm::Hour(h, is_12h) | TimeForm::HourMinute(h, 0, is_12h) => {
let hour = if *h == 0 { 23 } else { h.checked_sub(1)? };
let minute = 60_u32.checked_sub(mins)?;
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(
hour, minute, *is_12h,
))))
}
_ => None,
}
}),
},
Rule {
name: "<time:hour> <integer:minute>".to_string(),
pattern: vec![
predicate(
|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Hour(_, _))),
),
predicate(is_integer_between(0, 59)),
],
production: Box::new(|nodes| {
let (h, is_12h, is_latent) = match &nodes[0].token_data {
TokenData::Time(d) => match &d.form {
TimeForm::Hour(h, amb) => (*h, *amb, d.latent),
_ => return None,
},
_ => return None,
};
let m = numeral_data(&nodes[1].token_data)?.value as u32;
let td = if is_latent {
TimeData::latent(TimeForm::HourMinute(h, m, is_12h))
} else {
TimeData::new(TimeForm::HourMinute(h, m, is_12h))
};
Some(TokenData::Time(td))
}),
},
Rule {
name: "ISO interval".to_string(),
pattern: vec![regex(
r"\b(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\s*/\s*(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\b",
)],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let y1: i32 = m.group(1)?.parse().ok()?;
let mo1: u32 = m.group(2)?.parse().ok()?;
let d1: u32 = m.group(3)?.parse().ok()?;
let h1: u32 = m.group(4)?.parse().ok()?;
let mi1: u32 = m.group(5)?.parse().ok()?;
let s1: u32 = m.group(6)?.parse().ok()?;
let y2: i32 = m.group(7)?.parse().ok()?;
let mo2: u32 = m.group(8)?.parse().ok()?;
let d2: u32 = m.group(9)?.parse().ok()?;
let h2: u32 = m.group(10)?.parse().ok()?;
let mi2: u32 = m.group(11)?.parse().ok()?;
let s2: u32 = m.group(12)?.parse().ok()?;
let from = TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::HourMinuteSecond(h1, mi1, s1))),
Box::new(TimeData::new(TimeForm::DateMDY {
month: mo1,
day: d1,
year: Some(y1),
})),
));
let to = TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::HourMinuteSecond(h2, mi2, s2))),
Box::new(TimeData::new(TimeForm::DateMDY {
month: mo2,
day: d2,
year: Some(y2),
})),
));
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(from),
Box::new(to),
false,
))))
}),
},
Rule {
name: "digit<month> (no space)".to_string(),
pattern: vec![regex(
r"\b(\d{1,2})(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|oct|nov|dec)\b",
)],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let day: u32 = m.group(1)?.parse().ok()?;
let month_str = m.group(2)?;
let month = month_name_to_num(month_str)?;
if (1..=31).contains(&day) {
Some(TokenData::Time(TimeData::new(month_day(month, day))))
} else {
None
}
}),
},
Rule {
name: "<time> timezone".to_string(),
pattern: vec![
predicate(is_not_latent_time),
regex(
r"(?i)\b(cet|cest|gmt|utc|est|edt|cst|cdt|mst|mdt|pst|pdt|eet|eest|wet|west|bst|ist|jst|kst|hkt|sgt|aest|aedt|acst|acdt|awst|nzst|nzdt)\b",
),
],
production: Box::new(|nodes| {
let t = time_data(&nodes[0].token_data)?;
let tz_name = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(1)?.to_uppercase(),
_ => return None,
};
let mut new_t = t.clone();
new_t.timezone = Some(tz_name);
new_t.latent = false;
Some(TokenData::Time(new_t))
}),
},
Rule {
name: "<time> (timezone)".to_string(),
pattern: vec![
predicate(is_not_latent_time),
regex(
r"\((cet|cest|gmt|utc|est|edt|cst|cdt|mst|mdt|pst|pdt|eet|eest|wet|west|bst|ist|jst|kst|hkt|sgt|aest|aedt|acst|acdt|awst|nzst|nzdt)\)",
),
],
production: Box::new(|nodes| {
let t = time_data(&nodes[0].token_data)?;
let tz_name = match &nodes[1].token_data {
TokenData::RegexMatch(m) => m.group(1)?.to_uppercase(),
_ => return None,
};
let mut new_t = t.clone();
new_t.timezone = Some(tz_name);
new_t.latent = false;
Some(TokenData::Time(new_t))
}),
},
Rule {
name: "<time> - <time> timezone".to_string(),
pattern: vec![
dim(DimensionKind::Time),
regex(r"\s*[\-\u{2013}]\s*|to|th?ru|through|(un)?til(l)?"),
dim(DimensionKind::Time),
regex(
r"\b(cet|cest|gmt|utc|est|edt|cst|cdt|mst|mdt|pst|pdt|eet|eest|wet|west|bst|ist|jst|kst|hkt|sgt|aest|aedt|acst|acdt|awst|nzst|nzdt)\b",
),
],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[0].token_data)?;
let t2 = time_data(&nodes[2].token_data)?;
let tz_name = match &nodes[3].token_data {
TokenData::RegexMatch(m) => m.group(1)?.to_uppercase(),
_ => return None,
};
let mut from = t1.clone();
from.timezone = Some(tz_name.clone());
let mut to = t2.clone();
to.timezone = Some(tz_name.clone());
let mut iv = TimeData::new(TimeForm::Interval(Box::new(from), Box::new(to), false));
iv.timezone = Some(tz_name);
Some(TokenData::Time(iv))
}),
},
Rule {
name: "<datetime>/<datetime> (interval)".to_string(),
pattern: vec![
predicate(is_not_latent_time),
regex(r"\s*/\s*"),
predicate(is_not_latent_time),
],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[0].token_data)?;
let t2 = time_data(&nodes[2].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(t1.clone()),
Box::new(t2.clone()),
false,
))))
}),
},
Rule {
name: "<datetime> - <datetime> (interval) timezone".to_string(),
pattern: vec![
predicate(is_not_latent_time),
regex(r"\s*[\-\u{2013}]\s*|to|th?ru|through|(un)?til(l)?"),
predicate(is_not_latent_time),
regex(
r"\b(cet|cest|gmt|utc|est|edt|cst|cdt|mst|mdt|pst|pdt|eet|eest|wet|west|bst|ist|jst|kst|hkt|sgt|aest|aedt|acst|acdt|awst|nzst|nzdt)\b",
),
],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[0].token_data)?;
let t2 = time_data(&nodes[2].token_data)?;
let tz_name = match &nodes[3].token_data {
TokenData::RegexMatch(m) => m.group(1)?.to_uppercase(),
_ => return None,
};
let mut from = t1.clone();
from.timezone = Some(tz_name.clone());
let mut to = t2.clone();
to.timezone = Some(tz_name.clone());
let mut iv = TimeData::new(TimeForm::Interval(Box::new(from), Box::new(to), false));
iv.timezone = Some(tz_name);
Some(TokenData::Time(iv))
}),
},
Rule {
name: "<year> (latent) - <year> (latent) (interval)".to_string(),
pattern: vec![
predicate(|td| matches!(td, TokenData::Time(d) if d.latent && matches!(d.form, TimeForm::Year(_)))),
regex(r"\s*[\-\u{2013}]\s*"),
predicate(|td| matches!(td, TokenData::Time(d) if d.latent && matches!(d.form, TimeForm::Year(_)))),
],
production: Box::new(|nodes| {
let y1 = time_data(&nodes[0].token_data)?;
let y2 = time_data(&nodes[2].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(y1.clone()),
Box::new(y2.clone()),
false,
))))
}),
},
Rule {
name: "<day-of-month> (ordinal or number) of <named-month>".to_string(),
pattern: vec![
predicate(|td| matches!(td, TokenData::Ordinal(_) | TokenData::Numeral(_))),
regex(r"\bof\b"),
predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_)))),
],
production: Box::new(|nodes| {
let day = get_dom_value(&nodes[0].token_data)?;
let month_td = time_data(&nodes[2].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
Some(intersect_dom(month_td, day))
}),
},
Rule {
name: "<day-of-month> (ordinal or number) <named-month>".to_string(),
pattern: vec![
predicate(|td| matches!(td, TokenData::Ordinal(_) | TokenData::Numeral(_))),
predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_)))),
],
production: Box::new(|nodes| {
let day = get_dom_value(&nodes[0].token_data)?;
let month_td = time_data(&nodes[1].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
Some(intersect_dom(month_td, day))
}),
},
Rule {
name: "<day-of-month>(ordinal or number)/<named-month>/year".to_string(),
pattern: vec![
predicate(|td| matches!(td, TokenData::Ordinal(_) | TokenData::Numeral(_))),
predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_)))),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let day = get_dom_value(&nodes[0].token_data)?;
let month = match &nodes[1].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Month(m) => m,
_ => return None,
},
_ => return None,
};
let year = match &nodes[2].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Year(y) => y,
_ => return None,
},
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::DateMDY { month, day, year: Some(year) })))
}),
},
Rule {
name: "<day-of-month>(ordinal) <named-month> year".to_string(),
pattern: vec![dim(DimensionKind::Ordinal), predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Month(_)))), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let day = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as u32,
_ => return None,
};
let month = match &nodes[1].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Month(m) => m,
_ => return None,
},
_ => return None,
};
let year = match &nodes[2].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Year(y) => y,
_ => return None,
},
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::DateMDY { month, day, year: Some(year) })))
}),
},
Rule {
name: "<cycle> after|before <time>".to_string(),
pattern: vec![
dim(DimensionKind::TimeGrain),
regex(r"\b(after|before)\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let grain = match &nodes[0].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
let sign = match &nodes[1].token_data {
TokenData::RegexMatch(m) => if m.group(1)?.eq_ignore_ascii_case("before") { -1 } else { 1 },
_ => return None,
};
let base = time_data(&nodes[2].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthGrainOfTime {
n: sign,
grain,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "<ordinal> <cycle> of <time>".to_string(),
pattern: vec![dim(DimensionKind::Ordinal), dim(DimensionKind::TimeGrain), regex(r"\bof|in\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let n = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let grain = match &nodes[1].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
let base = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthGrainOfTime { n, grain, base: Box::new(base.clone()) })))
}),
},
Rule {
name: "<ordinal> last <cycle> of <time>".to_string(),
pattern: vec![dim(DimensionKind::Ordinal), regex(r"\blast\b"), dim(DimensionKind::TimeGrain), regex(r"\bof|in|from\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let n = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let grain = match &nodes[2].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
let base = time_data(&nodes[4].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthLastCycleOfTime { n, grain, base: Box::new(base.clone()) })))
}),
},
Rule {
name: "<integer> upcoming <cycle>".to_string(),
pattern: vec![predicate(is_integer_between(1, 9999)), regex(r"\bupcoming\b"), dim(DimensionKind::TimeGrain)],
production: Box::new(|nodes| {
let n = numeral_data(&nodes[0].token_data)?.value as i64;
let grain = match &nodes[2].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::NthGrain { n, grain, past: false, interval: false })))
}),
},
Rule {
name: "<duration> after|before|from|past <time>".to_string(),
pattern: vec![dim(DimensionKind::Duration), regex(r"\b(after|before|from|past)\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let dur = match &nodes[0].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
let sign = match &nodes[1].token_data {
TokenData::RegexMatch(m) => {
let w = m.group(1)?;
if w.eq_ignore_ascii_case("before") { -1 } else { 1 }
}
_ => return None,
};
let base = time_data(&nodes[2].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::DurationAfter {
n: dur.value.checked_mul(sign)?,
grain: dur.grain,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "<day> <duration> hence|ago".to_string(),
pattern: vec![predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::DayOfWeek(_)))), dim(DimensionKind::Duration), regex(r"\b(hence|ago)\b")],
production: Box::new(|nodes| {
let day = time_data(&nodes[0].token_data)?;
let dur = match &nodes[1].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
let sign = match &nodes[2].token_data {
TokenData::RegexMatch(m) => if m.group(1)?.eq_ignore_ascii_case("ago") { -1 } else { 1 },
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::DurationAfter {
n: dur.value.checked_mul(sign)?,
grain: dur.grain,
base: Box::new(day.clone()),
})))
}),
},
Rule {
name: "<day> in <duration>".to_string(),
pattern: vec![predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::DayOfWeek(_)))), regex(r"\bin\b"), dim(DimensionKind::Duration)],
production: Box::new(|nodes| {
let day = time_data(&nodes[0].token_data)?;
let dur = match &nodes[2].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::DurationAfter {
n: dur.value,
grain: dur.grain,
base: Box::new(day.clone()),
})))
}),
},
Rule {
name: "<hour-of-day> <integer>".to_string(),
pattern: vec![predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Hour(_, _) | TimeForm::HourMinute(_, 0, _)))), predicate(is_integer_between(10, 59))],
production: Box::new(|nodes| {
let (h, is12, latent) = match &nodes[0].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Hour(h, is12) => (h, is12, d.latent),
TimeForm::HourMinute(h, 0, is12) => (h, is12, d.latent),
_ => return None,
},
_ => return None,
};
let m = numeral_data(&nodes[1].token_data)?.value as u32;
let mut td = TimeData::new(TimeForm::HourMinute(h, m, is12));
td.latent = latent;
Some(TokenData::Time(td))
}),
},
Rule {
name: "<hour-of-day> half".to_string(),
pattern: vec![predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Hour(_, _) | TimeForm::HourMinute(_, 0, _)))), regex(r"\bhalf\b")],
production: Box::new(|nodes| {
let (h, is12) = match &nodes[0].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Hour(h, is12) => (h, is12),
TimeForm::HourMinute(h, 0, is12) => (h, is12),
_ => return None,
},
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(h, 30, is12))))
}),
},
Rule {
name: "<hour-of-day> quarter".to_string(),
pattern: vec![predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Hour(_, _) | TimeForm::HourMinute(_, 0, _)))), regex(r"\bquarter\b")],
production: Box::new(|nodes| {
let (h, is12) = match &nodes[0].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Hour(h, is12) => (h, is12),
TimeForm::HourMinute(h, 0, is12) => (h, is12),
_ => return None,
},
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(h, 15, is12))))
}),
},
Rule {
name: "<integer> to|till|before <hour-of-day>".to_string(),
pattern: vec![predicate(is_integer_between(1, 59)), regex(r"\b(to|till|before)\b"), predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Hour(_, _) | TimeForm::HourMinute(_, 0, _))))],
production: Box::new(|nodes| {
let mins = numeral_data(&nodes[0].token_data)?.value as u32;
let (h, is12) = match &nodes[2].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Hour(h, is12) => (h, is12),
TimeForm::HourMinute(h, 0, is12) => (h, is12),
_ => return None,
},
_ => return None,
};
let hour = if h == 0 { 23 } else { h.checked_sub(1)? };
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(hour, 60_u32.checked_sub(mins)?, is12))))
}),
},
Rule {
name: "<integer> minutes to|till|before <hour-of-day>".to_string(),
pattern: vec![predicate(is_integer_between(1, 59)), regex(r"\bminutes?\b"), regex(r"\b(to|till|before)\b"), predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Hour(_, _) | TimeForm::HourMinute(_, 0, _))))],
production: Box::new(|nodes| {
let mins = numeral_data(&nodes[0].token_data)?.value as u32;
let (h, is12) = match &nodes[3].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Hour(h, is12) => (h, is12),
TimeForm::HourMinute(h, 0, is12) => (h, is12),
_ => return None,
},
_ => return None,
};
let hour = if h == 0 { 23 } else { h.checked_sub(1)? };
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(hour, 60_u32.checked_sub(mins)?, is12))))
}),
},
Rule {
name: "<part-of-day> at <time-of-day>".to_string(),
pattern: vec![predicate(is_part_of_day), regex(r"\bat\b"), predicate(is_time_of_day)],
production: Box::new(|nodes| {
let p = time_data(&nodes[0].token_data)?;
let t = time_data(&nodes[2].token_data)?;
Some(TokenData::Time(compose(t, p)))
}),
},
Rule {
name: "<part-of-day> <latent-time-of-day> (latent)".to_string(),
pattern: vec![predicate(is_part_of_day), predicate(|td| matches!(td, TokenData::Time(d) if d.latent && is_time_of_day(td)))],
production: Box::new(|nodes| {
let p = time_data(&nodes[0].token_data)?;
let t = time_data(&nodes[1].token_data)?;
let mut out = compose(t, p);
out.latent = true;
Some(TokenData::Time(out))
}),
},
Rule {
name: "<time-of-day> <part-of-day>".to_string(),
pattern: vec![predicate(is_time_of_day), predicate(is_part_of_day)],
production: Box::new(|nodes| {
let t = time_data(&nodes[0].token_data)?;
let p = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(compose(t, p)))
}),
},
Rule {
name: "<time-of-day> sharp|exactly".to_string(),
pattern: vec![predicate(is_time_of_day), regex(r"\b(sharp|exactly)\b")],
production: Box::new(|nodes| {
let mut t = time_data(&nodes[0].token_data)?.clone();
t.latent = false;
Some(TokenData::Time(t))
}),
},
Rule {
name: "about|exactly <time-of-day>".to_string(),
pattern: vec![regex(r"\b(about|exactly)\b"), predicate(is_time_of_day)],
production: Box::new(|nodes| {
let mut t = time_data(&nodes[1].token_data)?.clone();
t.latent = false;
Some(TokenData::Time(t))
}),
},
Rule {
name: "<part-of-day> of <time>".to_string(),
pattern: vec![predicate(is_part_of_day), regex(r"\bof\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let p = time_data(&nodes[0].token_data)?;
let t = time_data(&nodes[2].token_data)?;
Some(TokenData::Time(compose(t, p)))
}),
},
Rule {
name: "from <time-of-day> - <time-of-day> (interval)".to_string(),
pattern: vec![
regex(r"\bfrom\b"),
predicate(is_time_of_day),
regex(r"\b(-|to|through)\b"),
predicate(is_time_of_day),
],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[1].token_data)?;
let t2 = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(t1.clone()),
Box::new(t2.clone()),
false,
))))
}),
},
Rule {
name: "last|past|next <duration>".to_string(),
pattern: vec![regex(r"\b([lp]ast|next)\b"), dim(DimensionKind::Duration)],
production: Box::new(|nodes| {
let direction = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
_ => return None,
};
let dur = match &nodes[1].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
let sign: i64 = if direction == "next" { 1 } else { -1 };
Some(TokenData::Time(TimeData::new(TimeForm::RelativeGrain {
n: sign.checked_mul(dur.value)?,
grain: dur.grain,
})))
}),
},
Rule {
name: "in <duration> at <time-of-day>".to_string(),
pattern: vec![regex(r"\bin\b"), dim(DimensionKind::Duration), regex(r"\bat\b"), predicate(is_time_of_day)],
production: Box::new(|nodes| {
let dur = match &nodes[1].token_data {
TokenData::Duration(d) => d,
_ => return None,
};
let tod = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Composed(
Box::new(TimeData::new(TimeForm::RelativeGrain {
n: dur.value,
grain: dur.grain,
})),
Box::new(tod.clone()),
))))
}),
},
Rule {
name: "<ordinal> <cycle> after <time>".to_string(),
pattern: vec![dim(DimensionKind::Ordinal), dim(DimensionKind::TimeGrain), regex(r"\bafter\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let n = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let grain = match &nodes[1].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
let base = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthGrainOfTime {
n,
grain,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "<ordinal> <cycle> after <time>".to_string(),
pattern: vec![regex(r"\bthe\b"), dim(DimensionKind::Ordinal), dim(DimensionKind::TimeGrain), regex(r"\bafter\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let n = match &nodes[1].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let grain = match &nodes[2].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
let base = time_data(&nodes[4].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthGrainOfTime {
n,
grain,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "the <cycle> after|before <time>".to_string(),
pattern: vec![regex(r"\bthe\b"), dim(DimensionKind::TimeGrain), regex(r"\b(after|before)\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let grain = match &nodes[1].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
let sign = match &nodes[2].token_data {
TokenData::RegexMatch(m) => {
if m.group(1)?.eq_ignore_ascii_case("before") { -1 } else { 1 }
}
_ => return None,
};
let base = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthGrainOfTime {
n: sign,
grain,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "the <cycle> of <time>".to_string(),
pattern: vec![regex(r"\bthe\b"), dim(DimensionKind::TimeGrain), regex(r"\bof\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let grain = match &nodes[1].token_data {
TokenData::TimeGrain(g) => *g,
_ => return None,
};
let base = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthGrainOfTime {
n: 0,
grain,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "the nth <time> after <time>".to_string(),
pattern: vec![regex(r"\bthe\b"), dim(DimensionKind::Ordinal), dim(DimensionKind::Time), regex(r"\bafter\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let n = match &nodes[1].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let t1 = time_data(&nodes[2].token_data)?;
let dow = match &t1.form {
TimeForm::DayOfWeek(d) => *d,
_ => return None,
};
let base = time_data(&nodes[4].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NDOWsFromTime {
n,
dow,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "<day-of-month> (ordinal or number) of <month>".to_string(),
pattern: vec![predicate(|td| get_dom_value(td).is_some()), regex(r"\bof( the)?\b"), predicate(is_month_token)],
production: Box::new(|nodes| {
let day = get_dom_value(&nodes[0].token_data)?;
let month_td = time_data(&nodes[2].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
Some(intersect_dom(month_td, day))
}),
},
Rule {
name: "intersect by \",\", \"of\", \"from\" for year".to_string(),
pattern: vec![predicate(is_not_latent_time), regex(r"\b(of|from|,)\b"), predicate(is_year_token)],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[0].token_data)?;
let t2 = time_data(&nodes[2].token_data)?;
Some(TokenData::Time(compose(t1, t2)))
}),
},
Rule {
name: "by the end of <time>".to_string(),
pattern: vec![regex(r"\bby( the)?\b"), dim(DimensionKind::Time)],
production: Box::new(|nodes| {
let t = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(TimeData::new(TimeForm::Now)),
Box::new(t.clone()),
false,
))))
}),
},
Rule {
name: "between <time-of-day> and <time-of-day> (interval)".to_string(),
pattern: vec![regex(r"\bbetween\b"), predicate(is_time_of_day), regex(r"\b(and|to)\b"), predicate(is_time_of_day)],
production: Box::new(|nodes| {
let t1 = time_data(&nodes[1].token_data)?;
let t2 = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(t1.clone()),
Box::new(t2.clone()),
false,
))))
}),
},
Rule {
name: "absorption of , after named day".to_string(),
pattern: vec![predicate(is_day_of_week), regex(r",")],
production: Box::new(|nodes| Some(nodes[0].token_data.clone())),
},
Rule {
name: "part of days".to_string(),
pattern: vec![regex(r"\b(morning|after ?noo?n(ish)?|evening|night|(at )?lunch)\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
_ => return None,
};
let pod = match m.as_str() {
"morning" => PartOfDay::Morning,
"evening" => PartOfDay::Evening,
"night" => PartOfDay::Night,
"lunch" | "at lunch" => PartOfDay::Lunch,
_ => PartOfDay::Afternoon,
};
Some(TokenData::Time(TimeData::latent(TimeForm::PartOfDay(pod))))
}),
},
Rule {
name: "early morning".to_string(),
pattern: vec![regex(r"\bearly ((in|hours of) the )?morning\b")],
production: Box::new(|_| {
let mut t = TimeData::latent(TimeForm::PartOfDay(PartOfDay::Morning));
t.early_late = Some(EarlyLate::Early);
Some(TokenData::Time(t))
}),
},
Rule {
name: "part of <named-month>".to_string(),
pattern: vec![regex(r"\b(early|mid|late)-?( of)?\b"), predicate(is_month_token)],
production: Box::new(|nodes| {
let kind = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
_ => return None,
};
let mut t = time_data(&nodes[1].token_data)?.clone();
t.latent = false;
t.early_late = Some(match kind.as_str() {
"late" => EarlyLate::Late,
"mid" => EarlyLate::Mid,
_ => EarlyLate::Early,
});
Some(TokenData::Time(t))
}),
},
Rule {
name: "at the beginning|end of <named-month>".to_string(),
pattern: vec![regex(r"\b(at the )?(beginning|end) of\b"), predicate(is_month_token)],
production: Box::new(|nodes| {
let begin = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(2)?.eq_ignore_ascii_case("beginning"),
_ => return None,
};
let t = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin,
target: Box::new(t.form.clone()),
})))
}),
},
Rule {
name: "at the beginning|end of <year>".to_string(),
pattern: vec![regex(r"\b(at the )?(beginning|end) of\b"), predicate(is_year_token)],
production: Box::new(|nodes| {
let begin = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(2)?.eq_ignore_ascii_case("beginning"),
_ => return None,
};
let t = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin,
target: Box::new(t.form.clone()),
})))
}),
},
Rule {
name: "at the beginning|end of <week>".to_string(),
pattern: vec![
regex(r"\b(at the )?(beginning|end) of\b"),
predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::GrainOffset { grain: Grain::Week, .. }))),
],
production: Box::new(|nodes| {
let begin = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m.group(2)?.eq_ignore_ascii_case("beginning"),
_ => return None,
};
let t = time_data(&nodes[1].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin,
target: Box::new(t.form.clone()),
})))
}),
},
Rule {
name: "beginning of month".to_string(),
pattern: vec![regex(r"\b((at )?the )?(BOM|beginning of (the )?month)\b")],
production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin: true,
target: Box::new(TimeForm::GrainOffset { grain: Grain::Month, offset: 0 }),
})))),
},
Rule {
name: "end of month".to_string(),
pattern: vec![regex(r"\b(by (the )?|(at )?the )?(EOM|end of (the )?month)\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let has_by = m.group(0)?.to_lowercase().starts_with("by");
let end_form = TimeForm::BeginEnd {
begin: false,
target: Box::new(TimeForm::GrainOffset { grain: Grain::Month, offset: 0 }),
};
if has_by {
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(TimeData::new(TimeForm::Now)),
Box::new(TimeData::new(end_form)),
false,
))))
} else {
Some(TokenData::Time(TimeData::new(end_form)))
}
}),
},
Rule {
name: "beginning of year".to_string(),
pattern: vec![regex(r"\b((at )?the )?(BOY|beginning of (the )?year)\b")],
production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::BeginEnd {
begin: true,
target: Box::new(TimeForm::GrainOffset { grain: Grain::Year, offset: 0 }),
})))),
},
Rule {
name: "end of year".to_string(),
pattern: vec![regex(r"\b(by (the )?|(at )?the )?(EOY|end of (the )?year)\b")],
production: Box::new(|nodes| {
let m = match &nodes[0].token_data {
TokenData::RegexMatch(m) => m,
_ => return None,
};
let has_by = m.group(0)?.to_lowercase().starts_with("by");
let end_form = TimeForm::BeginEnd {
begin: false,
target: Box::new(TimeForm::GrainOffset { grain: Grain::Year, offset: 0 }),
};
if has_by {
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(TimeData::new(TimeForm::Now)),
Box::new(TimeData::new(end_form)),
false,
))))
} else {
Some(TokenData::Time(TimeData::new(end_form)))
}
}),
},
Rule {
name: "<hour-of-day> zero <integer>".to_string(),
pattern: vec![
predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Hour(_, _) | TimeForm::HourMinute(_, 0, _)))),
regex(r"\b(zero|o(h|u)?)\b"),
predicate(is_integer_between(1, 9)),
],
production: Box::new(|nodes| {
let (h, is12) = match &nodes[0].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Hour(h, is12) => (h, is12),
TimeForm::HourMinute(h, 0, is12) => (h, is12),
_ => return None,
},
_ => return None,
};
let mins = numeral_data(&nodes[2].token_data)?.value as u32;
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(h, mins, is12))))
}),
},
Rule {
name: "<hour-of-day> - <integer-as-word>".to_string(),
pattern: vec![
predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Hour(_, _) | TimeForm::HourMinute(_, 0, _)))),
regex(r"-"),
predicate(is_integer_between(10, 59)),
],
production: Box::new(|nodes| {
let (h, is12) = match &nodes[0].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Hour(h, is12) => (h, is12),
TimeForm::HourMinute(h, 0, is12) => (h, is12),
_ => return None,
},
_ => return None,
};
let mins = numeral_data(&nodes[2].token_data)?.value as u32;
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(h, mins, is12))))
}),
},
Rule {
name: "<hour-of-day> - zero - <integer>".to_string(),
pattern: vec![
predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Hour(_, _) | TimeForm::HourMinute(_, 0, _)))),
regex(r"-"),
regex(r"\b(zero|o(h|u)?)\b"),
regex(r"-"),
predicate(is_integer_between(1, 9)),
],
production: Box::new(|nodes| {
let (h, is12) = match &nodes[0].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Hour(h, is12) => (h, is12),
TimeForm::HourMinute(h, 0, is12) => (h, is12),
_ => return None,
},
_ => return None,
};
let mins = numeral_data(&nodes[4].token_data)?.value as u32;
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(h, mins, is12))))
}),
},
Rule {
name: "half to|till|before <hour-of-day>".to_string(),
pattern: vec![
regex(r"\bhalf (to|till|before|of)\b"),
predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Hour(_, _) | TimeForm::HourMinute(_, 0, _)))),
],
production: Box::new(|nodes| {
let (h, is12) = match &nodes[1].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Hour(h, is12) => (h, is12),
TimeForm::HourMinute(h, 0, is12) => (h, is12),
_ => return None,
},
_ => return None,
};
let hour = if h == 0 { 23 } else { h.checked_sub(1)? };
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(hour, 30, is12))))
}),
},
Rule {
name: "half <integer> (UK style hour-of-day)".to_string(),
pattern: vec![
regex(r"\bhalf\b"),
predicate(|td| matches!(td, TokenData::Time(d) if matches!(d.form, TimeForm::Hour(_, _) | TimeForm::HourMinute(_, 0, _)))),
],
production: Box::new(|nodes| {
let (h, is12) = match &nodes[1].token_data {
TokenData::Time(d) => match d.form {
TimeForm::Hour(h, is12) => (h, is12),
TimeForm::HourMinute(h, 0, is12) => (h, is12),
_ => return None,
},
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(h, 30, is12))))
}),
},
Rule {
name: "nth <day-of-week> of <month-or-greater>".to_string(),
pattern: vec![
dim(DimensionKind::Ordinal),
predicate(is_day_of_week),
regex(r"\bof|in\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let n = match &nodes[0].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let dow = match &nodes[1].token_data {
TokenData::Time(d) => match d.form {
TimeForm::DayOfWeek(d) => d,
_ => return None,
},
_ => return None,
};
let base = time_data(&nodes[3].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthDOWOfTime {
n,
dow,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "the nth <day-of-week> of <month-or-greater>".to_string(),
pattern: vec![
regex(r"\bthe\b"),
dim(DimensionKind::Ordinal),
predicate(is_day_of_week),
regex(r"\bof|in\b"),
dim(DimensionKind::Time),
],
production: Box::new(|nodes| {
let n = match &nodes[1].token_data {
TokenData::Ordinal(d) => d.value as i32,
_ => return None,
};
let dow = match &nodes[2].token_data {
TokenData::Time(d) => match d.form {
TimeForm::DayOfWeek(d) => d,
_ => return None,
},
_ => return None,
};
let base = time_data(&nodes[4].token_data)?;
Some(TokenData::Time(TimeData::new(TimeForm::NthDOWOfTime {
n,
dow,
base: Box::new(base.clone()),
})))
}),
},
Rule {
name: "from the <day-of-month> (ordinal or number) to the <day-of-month> (ordinal or number) of <named-month> (interval)".to_string(),
pattern: vec![
regex(r"\bfrom\b"),
predicate(is_month_token),
predicate(|td| matches!(td, TokenData::Numeral(_) | TokenData::Ordinal(_))),
regex(r"\-|to( the)?|th?ru|through|(un)?til(l)?"),
predicate(|td| matches!(td, TokenData::Numeral(_) | TokenData::Ordinal(_))),
],
production: Box::new(|nodes| {
let month_td = time_data(&nodes[1].token_data)?;
if !matches!(month_td.form, TimeForm::Month(_)) {
return None;
}
let d1 = get_dom_value(&nodes[2].token_data)?;
let d2 = get_dom_value(&nodes[4].token_data)?;
let from = match intersect_dom(month_td, d1) {
TokenData::Time(td) => td,
_ => return None,
};
let to = match intersect_dom(month_td, d2) {
TokenData::Time(td) => td,
_ => return None,
};
Some(TokenData::Time(TimeData::new(TimeForm::Interval(
Box::new(from),
Box::new(to),
false,
))))
}),
},
Rule {
name: "Mid-day".to_string(),
pattern: vec![regex(r"\bmid[\s-]?day\b")],
production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(12, 0, false))))),
},
]
}
fn apply_ampm(form: &TimeForm, is_pm: bool) -> Option<TokenData> {
match form {
TimeForm::Hour(h, _) => {
let hour = if is_pm && *h < 12 {
h.checked_add(12)?
} else if !is_pm && *h == 12 {
0
} else {
*h
};
Some(TokenData::Time(TimeData::new(TimeForm::Hour(hour, false))))
}
TimeForm::HourMinute(h, m, _) => {
let hour = if is_pm && *h < 12 {
h.checked_add(12)?
} else if !is_pm && *h == 12 {
0
} else {
*h
};
Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(
hour, *m, false,
))))
}
TimeForm::HourMinuteSecond(h, m, s) => {
let hour = if is_pm && *h < 12 {
h.checked_add(12)?
} else if !is_pm && *h == 12 {
0
} else {
*h
};
Some(TokenData::Time(TimeData::new(TimeForm::HourMinuteSecond(
hour, *m, *s,
))))
}
_ => None,
}
}
fn text_to_grain(text: &str) -> Option<Grain> {
match text.to_lowercase().as_ref() {
"second" | "seconds" => Some(Grain::Second),
"minute" | "minutes" => Some(Grain::Minute),
"hour" | "hours" => Some(Grain::Hour),
"day" | "days" => Some(Grain::Day),
"week" | "weeks" => Some(Grain::Week),
"month" | "months" => Some(Grain::Month),
"quarter" | "quarters" | "qtr" | "qtrs" => Some(Grain::Quarter),
"year" | "years" | "yr" | "yrs" => Some(Grain::Year),
_ => None,
}
}
fn modifier_to_offset(text: &str) -> i32 {
match text.to_lowercase().as_ref() {
"last" | "past" | "previous" => -1,
"next" | "following" | "upcoming" | "coming" => 1,
"this" | "current" => 0,
_ => 0,
}
}
fn month_name_to_num(name: &str) -> Option<u32> {
match name.to_lowercase().as_ref() {
"jan" | "january" => Some(1),
"feb" | "february" => Some(2),
"mar" | "march" => Some(3),
"apr" | "april" => Some(4),
"may" => Some(5),
"jun" | "june" => Some(6),
"jul" | "july" => Some(7),
"aug" | "august" => Some(8),
"sep" | "september" => Some(9),
"oct" | "october" => Some(10),
"nov" | "november" => Some(11),
"dec" | "december" => Some(12),
_ => None,
}
}
fn holidays_regex() -> String {
let holidays = [
r"christmas(\s+day)?",
r"xmas(\s+day)?",
r"new\s+year'?s?\s+(eve|day)",
r"new\s+years?\s+(eve|day)",
r"valentine'?s?\s+day",
r"valentine\s+day",
r"halloween",
r"black\s+friday",
r"thanksgiving(\s+day)?",
r"(memorial|decoration)\s+day",
r"labou?r\s+day",
r"independence\s+day",
r"canada\s+day",
r"dominion\s+day",
r"anzac\s+day",
r"royal\s+hobart\s+regatta",
r"(royal\s+(national\s+agricultural|queensland)|rna)\s+show(\s+day)?",
r"ekka(\s+day)?",
r"reconciliation\s+day",
r"garifuna\s+settlement\s+day",
r"indian\s+arrival\s+day",
r"hosay",
r"rizal\s+day",
r"shivaji\s+jayanti",
r"hazarat\s+ali'?s\s+birthday",
r"(national\s+)?heroes'?s?\s+day",
r"day\s+of(\s+the)?\s+(covenant|reconciliation|vow)",
r"national\s+arbor\s+week",
r"naidoc\s+week",
r"kruger\s+day",
r"vimy\s+ridge\s+day",
r"(orangemen'?s?\s+day|the\s+(glorious\s+)?twelfth)",
r"(sovereign'?s?\s+birthday|victoria\s+day)",
r"discovery\s+day",
r"(civic\s+holiday|(british\s+columbia|civic|natal|new\s+brunswick|saskatchewan|terry\s+fox)\s+day)",
r"(family|islander|louis\s+riel|nova\s+scotia\s+heritage)\s+day",
r"national\s+patriot('?s|s')?\s+day",
r"labou?r\s+day\s+week(\s|-)?ends?",
r"heritage\s+day",
r"veterans?\s+day",
r"law\s+day",
r"lei\s+day",
r"loyalty\s+day",
r"george\s+washington\s+day",
r"washington'?s?\s+birthday",
r"presidents?'?\s+day",
r"daisy\s+gatson\s+bates\s+day",
r"lincoln'?s'?\s+birthday",
r"lincoln\s+birthday",
r"abraham\s+lincoln'?s'?\s+birthday",
r"guy\s+fawkes\s+day",
r"father'?s?\s+day",
r"mother'?s?\s+day",
r"mothering\s+sunday",
r"whitsunday",
r"(august|summer|late\s+summer)\s+bank\s+holiday",
r"national\s+grandparents\s+day",
r"military\s+spouse\s+day",
r"groundhogs?\s+day",
r"emancipation\s+day",
r"tax\s+day",
r"(national\s+)?sibling(s)?\s+day",
r"(administrative\s+professionals?'?|administrative\s+professional'?s?|secretaries'?|admins?)\s+day",
r"national\s+youth\s+service\s+day",
r"ems\s+week",
r"emsc\s+day",
r"daylight\s+savings?\s+(start|end)(\s+day)?",
r"(super|giga|mega\s+giga|super\s+duper|tsunami)\s+tue\.?(sday)?",
r"mini(\s*-\s*|\s+)tue\.?(sday)?",
r"boss'?s?(\s+day)?",
r"(mlk|martin\s+luther\s+king('?s)?,?)(\s+jr\.?|\s+junior)?\s+day",
r"(civil|idaho\s+human)\s+rights\s+day",
r"(saint|st\.?)\s+(patrick|paddy)'?s\s+day",
r"world\s+vegan\s+day",
r"easter(\s+(sunday|mon(day)?))?",
r"good\s+friday",
r"palm\s+sunday",
r"branch\s+sunday",
r"maundy\s+thursday",
r"covenant\s+thu(rsday)?",
r"thu(rsday)?\s+of\s+mysteries",
r"pentecost",
r"white\s+sunday",
r"whit\s+monday",
r"monday\s+of\s+the\s+holy\s+spirit",
r"trinity\s+sunday",
r"pancake\s+day",
r"mardi\s+gras",
r"shrove\s+tuesday",
r"lent",
r"ash\s+wednesday",
r"orthodox\s+(easter|good\s+friday|great\s+friday)",
r"(orthodox\s+)?(ash|clean|green|pure|shrove)\s+monday",
r"monday\s+of\s+lent",
r"lazarus\s+saturday",
r"great\s+fast",
r"chinese(\s+lunar)?\s+new\s+year'?s?(\s+day)?",
r"yom\s+kippur",
r"shemini\s+atzeret",
r"simchat\s+torah",
r"tisha\s+b'?av",
r"yom\s+haatzmaut",
r"lag\s+b'?omer",
r"yom\s+hashoah",
r"holocaust\s+day",
r"rosh\s+hashann?ah?",
r"yom\s+teruah",
r"chanukah",
r"hanuk(k)?ah",
r"hannuk(k)?ah",
r"passover",
r"(feast\s+of\s+the\s+ingathering|succos|sukkot)",
r"shavuot",
r"tu\s+bishvat",
r"purim",
r"shushan\s+purim",
r"mawlid(\s+al[\-\s]nabawi)?",
r"eid\s+al[\-\s]fitr",
r"eid\s+al[\-\s]adha",
r"id\s+ul[\-\s]adha",
r"sacrifice\s+feast",
r"bakr\s+id",
r"laylat\s+al[\-\s](qadr|kadr)",
r"night\s+of\s+(power|measures)",
r"islamic\s+new\s+year",
r"amun\s+jadid",
r"(day\s+of\s+)?ashura",
r"ramadan",
r"ramadh[ae]n",
r"ramzaan",
r"ramzan",
r"isra\s+and\s+mi'?raj",
r"the\s+prophet'?s\s+ascension",
r"the\s+night\s+journey",
r"ascension\s+to\s+heaven",
r"jumu'?atul[\-\s]wida",
r"jamat\s+ul[\-\s]vida",
r"dhanteras",
r"dhanatrayodashi",
r"diwali",
r"deepavali",
r"bhai\s+dooj",
r"chhathi?",
r"chhath\s+(parv|puja)",
r"dala\s+(chhath|puja)",
r"surya\s+shashthi",
r"navaratri",
r"durga\s+puja",
r"karva\s+chauth",
r"ratha[\-\s]yatra",
r"rakhi",
r"raksha\s+bandhan",
r"(mahavir|mahaveer)\s+(jayanti|janma\s+kalyanak)",
r"maha\s+shivaratri",
r"holi",
r"dhulandi",
r"phagwah",
r"chhoti\s+holi",
r"holika\s+dahan",
r"kamudu\s+pyre",
r"krishna\s+janmashtami",
r"gokulashtami",
r"ganesh\s+chaturthi",
r"rama\s+navami",
r"y?ugadi",
r"samvatsaradi",
r"chaitra\s+sukh?ladi",
r"pongal",
r"makara?\s+sankranth?i",
r"makar\s+sankranti",
r"maghi",
r"[bv]aisakhi",
r"vaisakhadi",
r"vasakhi",
r"vaishakhi",
r"mesadi",
r"(thiru(v|\s+))?onam",
r"vasant\s+panchami",
r"basant\s+panchami",
r"naraka?\s+(nivaran\s+)?chaturdashi",
r"(kali|roop)\s+chaudas",
r"choti\s+diwali",
r"maha\s+saptami",
r"dussehra",
r"vijayadashami",
r"saraswati\s+jayanti",
r"bogi\s+pandigai",
r"maattu\s+pongal",
r"kaanum\s+pongal",
r"kanni\s+pongal",
r"guru\s+gobind\s+singh\s+(birthday|jayanti)",
r"guru\s+govind\s+singh\s+jayanti",
r"guru\s+ravida?s(s)?\s+(jayanti|birthday)",
r"valmiki\s+jayanti",
r"maharishi\s+valmiki\s+jayanti",
r"pargat\s+diwas",
r"rabindra(nath)?\s+jayanti",
r"parsi\s+new\s+year",
r"jamshedi\s+navroz",
r"gysd",
r"global\s+youth\s+service\s+day",
r"vesak",
r"vaisakha",
r"buddha\s+(day|purnima)",
r"earth\s+hour",
r"koningsdag",
r"king's\s+day",
r"lakshmi\s+puja",
];
format!(r"\b({})\b", holidays.join("|"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dimensions::{duration, numeral, ordinal, time_grain};
use crate::engine;
use crate::resolve::{Context, Options};
use crate::types::DimensionKind;
fn all_rules() -> Vec<Rule> {
let mut r = numeral::en::rules();
r.extend(ordinal::en::rules());
r.extend(time_grain::en::rules());
r.extend(duration::en::rules());
r.extend(rules());
r
}
#[test]
fn test_days_of_week() {
let rules = all_rules();
let options = Options { with_latent: false };
let context = Context::default();
for day in &[
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
] {
let entities =
engine::parse_and_resolve(day, &rules, &context, &options, &[DimensionKind::Time]);
let found = entities.iter().any(|e| {
matches!(&e.value, crate::types::DimensionValue::Time(crate::types::TimeValue::Single { value: tp, .. }) if tp.grain() == crate::dimensions::time_grain::Grain::Day)
});
assert!(found, "Expected time for '{}', got: {:?}", day, entities);
}
}
#[test]
fn test_today_tomorrow() {
let rules = all_rules();
let options = Options { with_latent: false };
let context = Context::default();
for text in &["today", "tomorrow", "yesterday"] {
let entities =
engine::parse_and_resolve(text, &rules, &context, &options, &[DimensionKind::Time]);
let found = entities
.iter()
.any(|e| matches!(&e.value, crate::types::DimensionValue::Time(_)));
assert!(found, "Expected time for '{}', got: {:?}", text, entities);
}
}
#[test]
fn test_clock_time() {
let rules = all_rules();
let options = Options { with_latent: false };
let context = Context::default();
let entities =
engine::parse_and_resolve("3:30", &rules, &context, &options, &[DimensionKind::Time]);
let found = entities.iter().any(|e| {
matches!(&e.value, crate::types::DimensionValue::Time(crate::types::TimeValue::Single { value: tp, .. }) if tp.grain() == crate::dimensions::time_grain::Grain::Minute)
});
assert!(found, "Expected time for '3:30', got: {:?}", entities);
}
#[test]
fn test_in_duration() {
let rules = all_rules();
let options = Options { with_latent: false };
let context = Context::default();
let entities = engine::parse_and_resolve(
"in 3 days",
&rules,
&context,
&options,
&[DimensionKind::Time],
);
let found = entities
.iter()
.any(|e| matches!(&e.value, crate::types::DimensionValue::Time(_)));
assert!(found, "Expected time for 'in 3 days', got: {:?}", entities);
}
}