use jiff::civil::date;
use jiff::tz::TimeZone;
use jiff::{Span, Zoned};
use whenparse::{parse, parse_with, Dialect, Options, When};
const TZ: &str = "Asia/Yerevan";
fn anchor() -> Zoned {
date(2026, 8, 13)
.at(12, 0, 0, 0)
.to_zoned(TimeZone::get(TZ).unwrap())
.unwrap()
}
fn dt(y: i16, m: i8, d: i8, h: i8, mi: i8) -> When {
When::DateTime(
date(y, m, d)
.at(h, mi, 0, 0)
.to_zoned(TimeZone::get(TZ).unwrap())
.unwrap(),
)
}
fn range(s: (i16, i8, i8), e: (i16, i8, i8)) -> When {
let z = |y, m, d| {
date(y, m, d)
.at(0, 0, 0, 0)
.to_zoned(TimeZone::get(TZ).unwrap())
.unwrap()
};
When::Range {
start: z(s.0, s.1, s.2),
end: z(e.0, e.1, e.2),
}
}
fn check(cases: &[(&str, When)]) {
let now = anchor();
for (input, want) in cases {
assert_eq!(&parse_with(input, &now).unwrap(), want, "input: {input}");
}
}
#[test]
fn named_days() {
check(&[
("today", When::Date(date(2026, 8, 13))),
("Tomorrow", When::Date(date(2026, 8, 14))),
("yesterday", When::Date(date(2026, 8, 12))),
("сегодня", When::Date(date(2026, 8, 13))),
("завтра", When::Date(date(2026, 8, 14))),
("вчера", When::Date(date(2026, 8, 12))),
("послезавтра", When::Date(date(2026, 8, 15))),
("позавчера", When::Date(date(2026, 8, 11))),
]);
}
#[test]
fn relative_offsets() {
check(&[
("in 3 days", When::Date(date(2026, 8, 16))),
("3 days ago", When::Date(date(2026, 8, 10))),
("in 2 weeks", When::Date(date(2026, 8, 27))),
("in a week", When::Date(date(2026, 8, 20))),
("in two days", When::Date(date(2026, 8, 15))),
("in 3 months", When::Date(date(2026, 11, 13))),
("in an hour", dt(2026, 8, 13, 13, 0)),
("in 30 minutes", dt(2026, 8, 13, 12, 30)),
("in 45 min", dt(2026, 8, 13, 12, 45)),
]);
}
#[test]
fn weekdays() {
check(&[
("friday", When::Date(date(2026, 8, 14))),
("next friday", When::Date(date(2026, 8, 21))),
("last friday", When::Date(date(2026, 8, 7))),
("monday", When::Date(date(2026, 8, 17))),
("next monday", When::Date(date(2026, 8, 24))),
("tuesday", When::Date(date(2026, 8, 18))),
("this thursday", When::Date(date(2026, 8, 13))),
]);
}
#[test]
fn us_dialect_next_weekday() {
let now = anchor();
let opts = Options {
dialect: Dialect::Us,
..Options::default()
};
assert_eq!(
parse("next friday", &now, opts).unwrap(),
When::Date(date(2026, 8, 14))
);
}
#[test]
fn periods() {
check(&[
("next week", range((2026, 8, 17), (2026, 8, 24))),
("this week", range((2026, 8, 10), (2026, 8, 17))),
("last week", range((2026, 8, 3), (2026, 8, 10))),
("this month", range((2026, 8, 1), (2026, 9, 1))),
("last month", range((2026, 7, 1), (2026, 8, 1))),
("this year", range((2026, 1, 1), (2027, 1, 1))),
]);
}
#[test]
fn clock_and_at() {
check(&[
("noon", dt(2026, 8, 13, 12, 0)),
("midnight", dt(2026, 8, 13, 0, 0)),
("8:30", dt(2026, 8, 13, 8, 30)),
("at 8", dt(2026, 8, 13, 8, 0)),
("8pm", dt(2026, 8, 13, 20, 0)),
("8:30 pm", dt(2026, 8, 13, 20, 30)),
("tomorrow at 5pm", dt(2026, 8, 14, 17, 0)),
("next friday 8pm", dt(2026, 8, 21, 20, 0)),
("friday at noon", dt(2026, 8, 14, 12, 0)),
]);
}
#[test]
fn absolute_dates() {
check(&[
("15 March 2026", When::Date(date(2026, 3, 15))),
("March 15 2026", When::Date(date(2026, 3, 15))),
("March 15, 2026", When::Date(date(2026, 3, 15))),
("15 March", When::Date(date(2026, 3, 15))),
("March 15", When::Date(date(2026, 3, 15))),
("March 3rd", When::Date(date(2026, 3, 3))),
("the 3rd of March", When::Date(date(2026, 3, 3))),
("March", range((2026, 3, 1), (2026, 4, 1))),
("March 2026", range((2026, 3, 1), (2026, 4, 1))),
]);
}
#[test]
fn numeric_dates() {
check(&[
("15/03/2026", When::Date(date(2026, 3, 15))),
("15.03.2026", When::Date(date(2026, 3, 15))),
("2026/03/15", When::Date(date(2026, 3, 15))),
("3/15/2026", When::Date(date(2026, 3, 15))), ("15.03.26", When::Date(date(2026, 3, 15))),
]);
}
#[test]
fn us_numeric_date() {
let now = anchor();
let opts = Options {
dialect: Dialect::Us,
..Options::default()
};
assert_eq!(
parse("3/15/2026", &now, opts).unwrap(),
When::Date(date(2026, 3, 15))
);
assert_eq!(
parse("12/11/2026", &now, opts).unwrap(),
When::Date(date(2026, 12, 11)) );
}
#[test]
fn quarters() {
check(&[
("Q3", range((2026, 7, 1), (2026, 10, 1))),
("q3", range((2026, 7, 1), (2026, 10, 1))),
("Q1 2026", range((2026, 1, 1), (2026, 4, 1))),
("Q3 2025", range((2025, 7, 1), (2025, 10, 1))),
]);
}
#[test]
fn russian() {
check(&[
("через 3 дня", When::Date(date(2026, 8, 16))),
("3 дня назад", When::Date(date(2026, 8, 10))),
("через неделю", When::Date(date(2026, 8, 20))),
("неделю назад", When::Date(date(2026, 8, 6))),
("через два дня", When::Date(date(2026, 8, 15))),
("через 30 минут", dt(2026, 8, 13, 12, 30)),
("через час", dt(2026, 8, 13, 13, 0)),
("в следующую пятницу", When::Date(date(2026, 8, 21))),
("в прошлый вторник", When::Date(date(2026, 8, 11))),
("в пятницу", When::Date(date(2026, 8, 14))),
("среда", When::Date(date(2026, 8, 19))),
("на следующей неделе", range((2026, 8, 17), (2026, 8, 24))),
("в этом месяце", range((2026, 8, 1), (2026, 9, 1))),
("в прошлом году", range((2025, 1, 1), (2026, 1, 1))),
("15 марта", When::Date(date(2026, 3, 15))),
("15 марта 2026", When::Date(date(2026, 3, 15))),
("в 17:00", dt(2026, 8, 13, 17, 0)),
("в 8 вечера", dt(2026, 8, 13, 20, 0)),
("в 9 утра", dt(2026, 8, 13, 9, 0)),
("завтра в 17:00", dt(2026, 8, 14, 17, 0)),
("послезавтра в 9 утра", dt(2026, 8, 15, 9, 0)),
("в полдень", dt(2026, 8, 13, 12, 0)),
]);
}
fn dur(y: i64, mo: i64, w: i64, d: i64, h: i64, mi: i64, s: i64) -> When {
When::Duration(
Span::new()
.years(y)
.months(mo)
.weeks(w)
.days(d)
.hours(h)
.minutes(mi)
.seconds(s),
)
}
#[test]
fn durations() {
check(&[
("3 hours 20 minutes", dur(0, 0, 0, 0, 3, 20, 0)),
("2h30m", dur(0, 0, 0, 0, 2, 30, 0)),
("90 minutes", dur(0, 0, 0, 0, 0, 90, 0)),
("1h", dur(0, 0, 0, 0, 1, 0, 0)),
("3 days", dur(0, 0, 0, 3, 0, 0, 0)),
("3 часа 20 минут", dur(0, 0, 0, 0, 3, 20, 0)),
("2ч30м", dur(0, 0, 0, 0, 2, 30, 0)),
]);
}
#[test]
fn iso_passthrough() {
let now = anchor();
assert_eq!(
parse_with("2026-03-15", &now).unwrap(),
When::Date(date(2026, 3, 15))
);
}
#[test]
fn junk_is_unrecognized() {
let now = anchor();
assert!(parse_with("asdf qwer", &now).is_err());
assert!(parse_with("the day after banana", &now).is_err());
}