use jiff::civil::{date, Date, Weekday};
use jiff::tz::TimeZone;
use jiff::{Span, Zoned};
#[derive(Debug, Clone)]
pub enum When {
Date(Date),
DateTime(Zoned),
Range { start: Zoned, end: Zoned },
Duration(Span),
}
impl PartialEq for When {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(When::Date(a), When::Date(b)) => a == b,
(When::DateTime(a), When::DateTime(b)) => a == b,
(When::Range { start: s1, end: e1 }, When::Range { start: s2, end: e2 }) => {
s1 == s2 && e1 == e2
}
(When::Duration(a), When::Duration(b)) => a.fieldwise() == b.fieldwise(),
_ => false,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Lang {
#[default]
Auto,
En,
Ru,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Dialect {
#[default]
Intl,
Us,
}
#[derive(Debug, Clone, Copy)]
pub struct Options {
pub lang: Lang,
pub dialect: Dialect,
pub week_start: Weekday,
}
impl Default for Options {
fn default() -> Self {
Self {
lang: Lang::Auto,
dialect: Dialect::Intl,
week_start: Weekday::Monday,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
Unrecognized,
OutOfRange,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseError::Unrecognized => f.write_str("unrecognized date/time expression"),
ParseError::OutOfRange => f.write_str("date/time out of representable range"),
}
}
}
impl std::error::Error for ParseError {}
use ParseError::{OutOfRange as OOR, Unrecognized as UNK};
pub fn parse(input: &str, now: &Zoned, opts: Options) -> Result<When, ParseError> {
let toks = lex(input);
if toks.iter().any(|t| matches!(t, Tok::Unknown)) {
return iso(input);
}
match parse_expr(&toks) {
Some(expr) => resolve(&expr, now, opts),
None => iso(input),
}
}
pub fn parse_with(input: &str, now: &Zoned) -> Result<When, ParseError> {
parse(input, now, Options::default())
}
fn iso(input: &str) -> Result<When, ParseError> {
let s = input.trim();
if let Ok(z) = s.parse::<Zoned>() {
return Ok(When::DateTime(z));
}
if let Ok(d) = s.parse::<Date>() {
return Ok(When::Date(d));
}
Err(UNK)
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Unit {
Day,
Week,
Month,
Year,
Hour,
Minute,
Second,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Meridiem {
Am,
Pm,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Tok {
Num(i64),
Named(i64), Unit(Unit),
Weekday(Weekday),
Month(i8),
Quarter(i8),
NumDate(i64, i64, i64, bool), Clock(i8, i8, Option<Meridiem>),
Meridiem(Meridiem),
Noon,
Midnight,
In,
Ago,
Next,
Last,
This,
At,
Unknown,
}
fn lex(input: &str) -> Vec<Tok> {
let lowered = input.trim().to_lowercase().replace(',', " ");
let mut out = Vec::new();
for w in lowered.split_whitespace() {
if let Some(mut sub) = compact_duration(w) {
out.append(&mut sub);
} else if let Some(t) = classify(w) {
out.push(t);
}
}
out
}
fn compact_duration(w: &str) -> Option<Vec<Tok>> {
let mut chars = w.chars().peekable();
let mut out = Vec::new();
while chars.peek().is_some() {
let mut num = String::new();
while chars.peek().is_some_and(|c| c.is_ascii_digit()) {
num.push(chars.next().unwrap());
}
if num.is_empty() {
return None;
}
let mut letters = String::new();
while chars.peek().is_some_and(|c| c.is_alphabetic()) {
letters.push(chars.next().unwrap());
}
let unit = compact_unit(&letters)?;
out.push(Tok::Num(num.parse().ok()?));
out.push(Tok::Unit(unit));
}
(!out.is_empty()).then_some(out)
}
fn compact_unit(s: &str) -> Option<Unit> {
Some(match s {
"d" | "дн" | "д" => Unit::Day,
"w" | "н" => Unit::Week,
"h" | "ч" => Unit::Hour,
"m" | "м" => Unit::Minute,
"s" | "с" => Unit::Second,
_ => return None,
})
}
fn classify(w: &str) -> Option<Tok> {
if matches!(w, "on" | "the" | "of" | "в" | "во" | "на" | "к" | "тому") {
return None;
}
if let Ok(n) = w.parse::<i64>() {
return Some(Tok::Num(n));
}
if let Some(n) = ordinal(w) {
return Some(Tok::Num(n));
}
if let Some(t) = numdate_tok(w) {
return Some(t);
}
if let Some(t) = time_tok(w) {
return Some(t);
}
if let Some(t) = ru_word(w) {
return Some(t);
}
let tok = match w {
"today" | "tonight" | "сегодня" => Tok::Named(0),
"tomorrow" | "завтра" => Tok::Named(1),
"yesterday" | "вчера" => Tok::Named(-1),
"послезавтра" => Tok::Named(2),
"позавчера" => Tok::Named(-2),
"in" | "within" => Tok::In,
"ago" => Tok::Ago,
"next" => Tok::Next,
"last" | "previous" => Tok::Last,
"this" | "coming" => Tok::This,
"at" => Tok::At,
"noon" | "midday" => Tok::Noon,
"midnight" => Tok::Midnight,
"am" => Tok::Meridiem(Meridiem::Am),
"pm" => Tok::Meridiem(Meridiem::Pm),
"a" | "an" => Tok::Num(1),
"day" | "days" => Tok::Unit(Unit::Day),
"week" | "weeks" | "wk" => Tok::Unit(Unit::Week),
"month" | "months" | "mo" => Tok::Unit(Unit::Month),
"year" | "years" | "yr" => Tok::Unit(Unit::Year),
"hour" | "hours" | "hr" | "hrs" => Tok::Unit(Unit::Hour),
"minute" | "minutes" | "min" | "mins" => Tok::Unit(Unit::Minute),
"second" | "seconds" | "sec" | "secs" => Tok::Unit(Unit::Second),
"monday" | "mon" => Tok::Weekday(Weekday::Monday),
"tuesday" | "tue" | "tues" => Tok::Weekday(Weekday::Tuesday),
"wednesday" | "wed" => Tok::Weekday(Weekday::Wednesday),
"thursday" | "thu" | "thur" | "thurs" => Tok::Weekday(Weekday::Thursday),
"friday" | "fri" => Tok::Weekday(Weekday::Friday),
"saturday" | "sat" => Tok::Weekday(Weekday::Saturday),
"sunday" | "sun" => Tok::Weekday(Weekday::Sunday),
"january" | "jan" => Tok::Month(1),
"february" | "feb" => Tok::Month(2),
"march" | "mar" => Tok::Month(3),
"april" | "apr" => Tok::Month(4),
"may" => Tok::Month(5),
"june" | "jun" => Tok::Month(6),
"july" | "jul" => Tok::Month(7),
"august" | "aug" => Tok::Month(8),
"september" | "sep" | "sept" => Tok::Month(9),
"october" | "oct" => Tok::Month(10),
"november" | "nov" => Tok::Month(11),
"december" | "dec" => Tok::Month(12),
"q1" => Tok::Quarter(1),
"q2" => Tok::Quarter(2),
"q3" => Tok::Quarter(3),
"q4" => Tok::Quarter(4),
_ => match number_word(w) {
Some(n) => Tok::Num(n),
None => Tok::Unknown,
},
};
Some(tok)
}
fn ordinal(w: &str) -> Option<i64> {
for suf in ["st", "nd", "rd", "th"] {
if let Some(p) = w.strip_suffix(suf) {
if !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()) {
return p.parse().ok();
}
}
}
None
}
fn numdate_tok(w: &str) -> Option<Tok> {
for (sep, dotted) in [('/', false), ('.', true)] {
if !w.contains(sep) {
continue;
}
let parts: Vec<&str> = w.split(sep).collect();
let numeric = parts.len() == 3
&& parts
.iter()
.all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()));
if numeric {
let a = parts[0].parse().ok()?;
let b = parts[1].parse().ok()?;
let c = parts[2].parse().ok()?;
return Some(Tok::NumDate(a, b, c, dotted));
}
}
None
}
fn ru_word(w: &str) -> Option<Tok> {
let tok = match w {
"через" | "спустя" => Tok::In,
"назад" => Tok::Ago,
"следующий" | "следующая" | "следующее" | "следующую" | "следующем" | "следующей"
| "след" | "будущий" | "будущая" | "будущем" | "будущую" => {
Tok::Next
}
"прошлый" | "прошлая" | "прошлое" | "прошлую" | "прошлом" | "прошлой" | "прошедшая"
| "прошедшую" | "минувшую" => Tok::Last,
"этот" | "эта" | "это" | "эту" | "этом" | "этой" | "этого" | "текущий" | "текущем" => {
Tok::This
}
"полдень" | "полудень" => Tok::Noon,
"полночь" | "полуночь" => Tok::Midnight,
"утра" | "утро" | "утром" | "ночи" => Tok::Meridiem(Meridiem::Am),
"вечера" | "вечер" | "вечером" => Tok::Meridiem(Meridiem::Pm),
"день" | "дня" | "дней" | "дни" | "суток" | "сутки" => {
Tok::Unit(Unit::Day)
}
"неделя" | "недели" | "неделю" | "недель" | "неделе" | "неделей" => {
Tok::Unit(Unit::Week)
}
"месяц" | "месяца" | "месяцев" | "месяце" | "месяцы" => {
Tok::Unit(Unit::Month)
}
"год" | "года" | "лет" | "году" | "годов" | "годы" => {
Tok::Unit(Unit::Year)
}
"час" | "часа" | "часов" | "часу" => Tok::Unit(Unit::Hour),
"минута" | "минуты" | "минут" | "минуту" | "минуте" | "мин" => {
Tok::Unit(Unit::Minute)
}
"секунда" | "секунды" | "секунд" | "секунду" | "сек" => {
Tok::Unit(Unit::Second)
}
"понедельник" | "понедельника" => Tok::Weekday(Weekday::Monday),
"вторник" | "вторника" => Tok::Weekday(Weekday::Tuesday),
"среда" | "среду" | "среды" => Tok::Weekday(Weekday::Wednesday),
"четверг" | "четверга" => Tok::Weekday(Weekday::Thursday),
"пятница" | "пятницу" | "пятницы" => Tok::Weekday(Weekday::Friday),
"суббота" | "субботу" | "субботы" => Tok::Weekday(Weekday::Saturday),
"воскресенье" | "воскресенья" | "воскресение" => {
Tok::Weekday(Weekday::Sunday)
}
"январь" | "января" => Tok::Month(1),
"февраль" | "февраля" => Tok::Month(2),
"март" | "марта" => Tok::Month(3),
"апрель" | "апреля" => Tok::Month(4),
"май" | "мая" => Tok::Month(5),
"июнь" | "июня" => Tok::Month(6),
"июль" | "июля" => Tok::Month(7),
"август" | "августа" => Tok::Month(8),
"сентябрь" | "сентября" => Tok::Month(9),
"октябрь" | "октября" => Tok::Month(10),
"ноябрь" | "ноября" => Tok::Month(11),
"декабрь" | "декабря" => Tok::Month(12),
"один" | "одна" | "одно" | "одну" => Tok::Num(1),
"два" | "две" => Tok::Num(2),
"три" => Tok::Num(3),
"четыре" => Tok::Num(4),
"пять" => Tok::Num(5),
"шесть" => Tok::Num(6),
"семь" => Tok::Num(7),
"восемь" => Tok::Num(8),
"девять" => Tok::Num(9),
"десять" => Tok::Num(10),
"одиннадцать" => Tok::Num(11),
"двенадцать" => Tok::Num(12),
_ => return None,
};
Some(tok)
}
fn number_word(w: &str) -> Option<i64> {
Some(match w {
"one" => 1,
"two" => 2,
"three" => 3,
"four" => 4,
"five" => 5,
"six" => 6,
"seven" => 7,
"eight" => 8,
"nine" => 9,
"ten" => 10,
"eleven" => 11,
"twelve" => 12,
_ => return None,
})
}
fn time_tok(w: &str) -> Option<Tok> {
let (core, mer) = if let Some(s) = w.strip_suffix("pm") {
(s, Some(Meridiem::Pm))
} else if let Some(s) = w.strip_suffix("am") {
(s, Some(Meridiem::Am))
} else {
(w, None)
};
if let Some((hs, ms)) = core.split_once(':') {
let h = hs.parse::<i8>().ok()?;
let m = ms.parse::<i8>().ok()?;
if (0..=23).contains(&h) && (0..=59).contains(&m) {
return Some(Tok::Clock(h, m, mer));
}
return None;
}
if mer.is_some() && !core.is_empty() && core.bytes().all(|b| b.is_ascii_digit()) {
let h = core.parse::<i8>().ok()?;
if (0..=23).contains(&h) {
return Some(Tok::Clock(h, 0, mer));
}
}
None
}
#[derive(Debug, Clone, Copy)]
enum WeekRel {
Coming,
Next,
Last,
This,
}
#[derive(Debug, Clone)]
enum Expr {
DayOffset(i64),
Offset(i64, Unit),
Weekday(Weekday, WeekRel),
Period(i64, Unit),
Clock(i8, i8, Option<Meridiem>),
At(Box<Expr>, Box<Expr>),
NumDate(i64, i64, i64, bool),
Ymd(i64, i8, i64), MonthDay(i8, i64),
MonthNum(i8, i64), MonthWhole(i8),
Quarter(i8, Option<i64>),
Duration(Vec<(i64, Unit)>),
}
fn parse_expr(toks: &[Tok]) -> Option<Expr> {
if toks.is_empty() {
return None;
}
if let Some((date_len, clock)) = split_trailing_clock(toks) {
if date_len == 0 {
return Some(clock);
}
let d = parse_date_expr(&toks[..date_len])?;
return Some(Expr::At(Box::new(d), Box::new(clock)));
}
if let Some(d) = parse_duration(toks) {
return Some(d);
}
parse_date_expr(toks)
}
fn parse_duration(toks: &[Tok]) -> Option<Expr> {
if toks.is_empty() || !toks.len().is_multiple_of(2) {
return None;
}
let mut parts = Vec::new();
for pair in toks.chunks_exact(2) {
match pair {
[Tok::Num(n), Tok::Unit(u)] => parts.push((*n, *u)),
_ => return None,
}
}
Some(Expr::Duration(parts))
}
fn split_trailing_clock(toks: &[Tok]) -> Option<(usize, Expr)> {
let n = toks.len();
let (core_end, mer) = match toks[n - 1] {
Tok::Meridiem(m) => (n - 1, Some(m)),
_ => (n, None),
};
if core_end == 0 {
return None;
}
let (h, m, cmer, core_start) = match toks[core_end - 1] {
Tok::Noon => (12, 0, None, core_end - 1),
Tok::Midnight => (0, 0, None, core_end - 1),
Tok::Clock(h, m, cm) => (h, m, cm.or(mer), core_end - 1),
Tok::Num(v) if (0..=23).contains(&v) => (v as i8, 0, mer, core_end - 1),
_ => return None,
};
let preceded_by_at = core_start > 0 && toks[core_start - 1] == Tok::At;
let justified = mer.is_some()
|| matches!(toks[core_start], Tok::Clock(..) | Tok::Noon | Tok::Midnight)
|| preceded_by_at;
if !justified {
return None;
}
let date_len = if preceded_by_at {
core_start - 1
} else {
core_start
};
Some((date_len, Expr::Clock(h, m, cmer)))
}
fn parse_date_expr(toks: &[Tok]) -> Option<Expr> {
match toks {
[Tok::Named(n)] => Some(Expr::DayOffset(*n)),
[Tok::In, Tok::Num(n), Tok::Unit(u)] => Some(Expr::Offset(*n, *u)),
[Tok::In, Tok::Unit(u)] => Some(Expr::Offset(1, *u)),
[Tok::Num(n), Tok::Unit(u), Tok::Ago] => Some(Expr::Offset(-(*n), *u)),
[Tok::Unit(u), Tok::Ago] => Some(Expr::Offset(-1, *u)),
[Tok::Next, Tok::Weekday(w)] => Some(Expr::Weekday(*w, WeekRel::Next)),
[Tok::Last, Tok::Weekday(w)] => Some(Expr::Weekday(*w, WeekRel::Last)),
[Tok::This, Tok::Weekday(w)] => Some(Expr::Weekday(*w, WeekRel::This)),
[Tok::Weekday(w)] => Some(Expr::Weekday(*w, WeekRel::Coming)),
[Tok::Next, Tok::Unit(u)] if is_period(*u) => Some(Expr::Period(1, *u)),
[Tok::Last, Tok::Unit(u)] if is_period(*u) => Some(Expr::Period(-1, *u)),
[Tok::This, Tok::Unit(u)] if is_period(*u) => Some(Expr::Period(0, *u)),
[Tok::NumDate(a, b, c, dotted)] => Some(Expr::NumDate(*a, *b, *c, *dotted)),
[Tok::Num(d), Tok::Month(m), Tok::Num(y)] => Some(Expr::Ymd(*y, *m, *d)),
[Tok::Month(m), Tok::Num(d), Tok::Num(y)] => Some(Expr::Ymd(*y, *m, *d)),
[Tok::Num(d), Tok::Month(m)] => Some(Expr::MonthDay(*m, *d)),
[Tok::Month(m), Tok::Num(x)] => Some(Expr::MonthNum(*m, *x)),
[Tok::Month(m)] => Some(Expr::MonthWhole(*m)),
[Tok::Quarter(q)] => Some(Expr::Quarter(*q, None)),
[Tok::Quarter(q), Tok::Num(y)] => Some(Expr::Quarter(*q, Some(*y))),
_ => None,
}
}
fn is_period(u: Unit) -> bool {
matches!(u, Unit::Week | Unit::Month | Unit::Year)
}
fn resolve(expr: &Expr, now: &Zoned, opts: Options) -> Result<When, ParseError> {
let today = now.date();
let tz = now.time_zone().clone();
match expr {
Expr::DayOffset(n) => dadd(today, Span::new().days(*n)).map(When::Date),
Expr::Offset(n, u) => resolve_offset(today, now, *n, *u),
Expr::Weekday(w, rel) => resolve_weekday(today, *w, *rel, opts.dialect).map(When::Date),
Expr::Period(n, u) => resolve_period(today, *n, *u, opts.week_start, &tz),
Expr::Clock(h, m, mer) => {
let (h, m) = clock24(*h, *m, *mer)?;
at_time(today, h, m, &tz).map(When::DateTime)
}
Expr::At(d, c) => {
let base = match resolve(d, now, opts)? {
When::Date(d) => d,
_ => return Err(UNK),
};
let (h, m) = match **c {
Expr::Clock(h, m, mer) => clock24(h, m, mer)?,
_ => return Err(UNK),
};
at_time(base, h, m, &tz).map(When::DateTime)
}
Expr::NumDate(a, b, c, dotted) => {
resolve_numdate(*a, *b, *c, *dotted, opts.dialect).map(When::Date)
}
Expr::Ymd(y, m, d) => mkdate(*y, *m, *d).map(When::Date),
Expr::MonthDay(m, d) => mkdate(today.year() as i64, *m, *d).map(When::Date),
Expr::MonthNum(m, x) => {
if (1..=31).contains(x) {
mkdate(today.year() as i64, *m, *x).map(When::Date)
} else {
month_range(norm_year(*x), *m, &tz)
}
}
Expr::MonthWhole(m) => month_range(today.year() as i64, *m, &tz),
Expr::Quarter(q, y) => {
let year = match y {
Some(v) => norm_year(*v),
None => today.year() as i64,
};
quarter_range(year, *q, &tz)
}
Expr::Duration(parts) => Ok(When::Duration(build_span(parts))),
}
}
fn build_span(parts: &[(i64, Unit)]) -> Span {
let (mut y, mut mo, mut w, mut d, mut h, mut mi, mut s) = (0i64, 0, 0, 0, 0, 0, 0);
for (n, u) in parts {
match u {
Unit::Year => y += n,
Unit::Month => mo += n,
Unit::Week => w += n,
Unit::Day => d += n,
Unit::Hour => h += n,
Unit::Minute => mi += n,
Unit::Second => s += n,
}
}
Span::new()
.years(y)
.months(mo)
.weeks(w)
.days(d)
.hours(h)
.minutes(mi)
.seconds(s)
}
fn norm_year(y: i64) -> i64 {
if (0..100).contains(&y) {
2000 + y
} else {
y
}
}
fn mkdate(y: i64, m: i8, d: i64) -> Result<Date, ParseError> {
let y: i16 = y.try_into().map_err(|_| OOR)?;
let d: i8 = d.try_into().map_err(|_| OOR)?;
Date::new(y, m, d).map_err(|_| UNK)
}
fn resolve_numdate(
a: i64,
b: i64,
c: i64,
dotted: bool,
dialect: Dialect,
) -> Result<Date, ParseError> {
let (year, mut day, mut month) = if a >= 1000 {
(a, c, b) } else {
let year = norm_year(c);
let (day, month) = if dotted || dialect == Dialect::Intl {
(a, b) } else {
(b, a) };
(year, day, month)
};
if month > 12 && day <= 12 {
std::mem::swap(&mut day, &mut month);
}
let month: i8 = month.try_into().map_err(|_| UNK)?;
mkdate(year, month, day)
}
fn month_range(y: i64, m: i8, tz: &TimeZone) -> Result<When, ParseError> {
let y: i16 = y.try_into().map_err(|_| OOR)?;
let start = Date::new(y, m, 1).map_err(|_| UNK)?;
let end = dadd(start, Span::new().months(1))?;
Ok(When::Range {
start: midnight(start, tz)?,
end: midnight(end, tz)?,
})
}
fn quarter_range(y: i64, q: i8, tz: &TimeZone) -> Result<When, ParseError> {
if !(1..=4).contains(&q) {
return Err(UNK);
}
let y: i16 = y.try_into().map_err(|_| OOR)?;
let start = Date::new(y, (q - 1) * 3 + 1, 1).map_err(|_| UNK)?;
let end = dadd(start, Span::new().months(3))?;
Ok(When::Range {
start: midnight(start, tz)?,
end: midnight(end, tz)?,
})
}
fn resolve_offset(today: Date, now: &Zoned, n: i64, u: Unit) -> Result<When, ParseError> {
match u {
Unit::Day => dadd(today, Span::new().days(n)).map(When::Date),
Unit::Week => dadd(today, Span::new().weeks(n)).map(When::Date),
Unit::Month => dadd(today, Span::new().months(n)).map(When::Date),
Unit::Year => dadd(today, Span::new().years(n)).map(When::Date),
Unit::Hour => zadd(now, Span::new().hours(n)).map(When::DateTime),
Unit::Minute => zadd(now, Span::new().minutes(n)).map(When::DateTime),
Unit::Second => zadd(now, Span::new().seconds(n)).map(When::DateTime),
}
}
fn resolve_weekday(
today: Date,
target: Weekday,
rel: WeekRel,
dialect: Dialect,
) -> Result<Date, ParseError> {
let cur = wnum(today.weekday());
let tgt = wnum(target);
let fwd = (tgt - cur).rem_euclid(7); let delta = match rel {
WeekRel::Coming | WeekRel::This => fwd,
WeekRel::Next => match dialect {
Dialect::Us => {
if fwd == 0 {
7
} else {
fwd
}
}
Dialect::Intl => fwd + 7,
},
WeekRel::Last => {
let back = (cur - tgt).rem_euclid(7);
-(if back == 0 { 7 } else { back })
}
};
dadd(today, Span::new().days(delta as i64))
}
fn resolve_period(
today: Date,
n: i64,
u: Unit,
week_start: Weekday,
tz: &TimeZone,
) -> Result<When, ParseError> {
let (start, end) = match u {
Unit::Week => {
let back = (wnum(today.weekday()) - wnum(week_start)).rem_euclid(7);
let s = dadd(today, Span::new().days(-(back as i64) + n * 7))?;
(s, dadd(s, Span::new().days(7))?)
}
Unit::Month => {
let base = date(today.year(), today.month(), 1);
let s = dadd(base, Span::new().months(n))?;
(s, dadd(s, Span::new().months(1))?)
}
Unit::Year => {
let base = date(today.year(), 1, 1);
let s = dadd(base, Span::new().years(n))?;
(s, dadd(s, Span::new().years(1))?)
}
_ => return Err(UNK),
};
Ok(When::Range {
start: midnight(start, tz)?,
end: midnight(end, tz)?,
})
}
fn clock24(h: i8, m: i8, mer: Option<Meridiem>) -> Result<(i8, i8), ParseError> {
let h = match mer {
Some(Meridiem::Pm) => {
if h == 12 {
12
} else {
h + 12
}
}
Some(Meridiem::Am) => {
if h == 12 {
0
} else {
h
}
}
None => h,
};
if (0..=23).contains(&h) && (0..=59).contains(&m) {
Ok((h, m))
} else {
Err(UNK)
}
}
fn wnum(w: Weekday) -> i32 {
match w {
Weekday::Monday => 0,
Weekday::Tuesday => 1,
Weekday::Wednesday => 2,
Weekday::Thursday => 3,
Weekday::Friday => 4,
Weekday::Saturday => 5,
Weekday::Sunday => 6,
}
}
fn dadd(d: Date, sp: Span) -> Result<Date, ParseError> {
d.checked_add(sp).map_err(|_| OOR)
}
fn zadd(z: &Zoned, sp: Span) -> Result<Zoned, ParseError> {
z.checked_add(sp).map_err(|_| OOR)
}
fn at_time(d: Date, h: i8, m: i8, tz: &TimeZone) -> Result<Zoned, ParseError> {
d.at(h, m, 0, 0).to_zoned(tz.clone()).map_err(|_| OOR)
}
fn midnight(d: Date, tz: &TimeZone) -> Result<Zoned, ParseError> {
d.at(0, 0, 0, 0).to_zoned(tz.clone()).map_err(|_| OOR)
}