use dtrexp::{parse, validate, Tz};
fn utc_ms(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64) -> i64 {
let yy = y - i64::from(mo <= 2);
let era = if yy >= 0 { yy } else { yy - 399 } / 400;
let yoe = yy - era * 400;
let doy = (153 * (mo + if mo > 2 { -3 } else { 9 }) + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
let days = era * 146097 + doe - 719468;
days * 86_400_000 + h * 3_600_000 + mi * 60_000 + s * 1_000
}
fn err(expr: &str) {
assert!(parse(expr).is_err(), "`{expr}` should be rejected");
}
fn err_msg(expr: &str, message: &str) {
match parse(expr) {
Ok(_) => panic!("`{expr}` should be rejected"),
Err(e) => assert_eq!(e.message, message, "`{expr}` rejected for the wrong reason"),
}
}
fn quiet(expr: &str) {
match parse(expr) {
Ok(p) => assert!(
p.warnings().is_empty(),
"`{expr}` should be quiet, warned: {:?}",
p.warnings()
),
Err(e) => panic!("`{expr}` should parse, errored: {e}"),
}
}
fn warns(expr: &str) {
match parse(expr) {
Ok(p) => assert!(!p.warnings().is_empty(), "`{expr}` should warn"),
Err(e) => panic!("`{expr}` should parse-with-warning, errored: {e}"),
}
}
#[test]
fn selector_syntax_errors() {
err("M!3/2"); err("E1:3#2"); err("M3,*"); err("M*:5/2"); err("M5:3/2"); err("M10000001"); err("M-"); err("M13"); }
#[test]
fn time_component_errors() {
err("T0900 T1000"); err("T*"); err("T"); err("T1"); err("T006000"); err("T000060"); err("T120000.5"); }
#[test]
fn bounds_and_cadence_errors() {
err("*"); err("*5"); err("*:*"); err("202001"); err("20200101T000099"); err("20200101/2X"); err("20200101/2D/0D"); }
#[test]
fn year_selector_errors() {
const WRAP: &str = "Y range cannot wrap — no edge to wrap around";
const DOMAIN: &str = "year out of domain (1-9999)";
err_msg("Y2020:2010", WRAP); err_msg("Y2020:10000", DOMAIN); err_msg("Y0:2020", DOMAIN); err_msg("Y10000:2020", WRAP);
err("Y2000/1"); err("Y2000/4/4"); err("Y10000/4/2"); err("Y2000:10000/4/2"); err("E8#2"); }
#[test]
fn domain_bounds_on_ranges_and_strides() {
quiet("M3:7"); quiet("H0:20/4"); quiet("Y2000:3000/4/2"); err("M0:7"); err("M3:99"); err("M0/4"); err("M3:99/4"); }
#[test]
fn day_without_month_scope_uses_the_full_month_set() {
quiet("D31 E1"); }
#[test]
fn month_quarter_disjoint_warns() {
warns("M2 Q3");
}
#[test]
fn day_never_exists_warns() {
warns("Y2020 M2 D30"); warns("Y2020 Q1 D92"); warns("Y2021 D366"); }
#[test]
fn open_year_spans_stay_quiet() {
quiet("M!3,5"); quiet("Y!2020 D31"); quiet("Y1:2000 D31"); quiet("Y1,2000 D31"); quiet("Y*:2020"); quiet("M2 Q1"); }
#[test]
fn exclude_everything_is_unsatisfiable() {
warns("M!*"); }
#[test]
fn duplicate_bounds_are_rejected() {
err("*:20200301 *:20200401"); }
#[test]
fn last_weekday_ordinal() {
let expr = parse("E1#-1").unwrap(); let utc = Tz::utc();
assert!(expr.covers_in(utc_ms(2026, 7, 27, 12, 0, 0), &utc));
assert!(!expr.covers_in(utc_ms(2026, 7, 20, 12, 0, 0), &utc));
assert!(!expr.covers_in(utc_ms(2026, 7, 28, 12, 0, 0), &utc));
}
#[test]
fn year_scoped_last_weekday_ordinal() {
let expr = parse("Y2026 E1#-1").unwrap(); let utc = Tz::utc();
assert!(expr.covers_in(utc_ms(2026, 12, 28, 12, 0, 0), &utc)); assert!(!expr.covers_in(utc_ms(2026, 12, 21, 12, 0, 0), &utc)); }
#[test]
fn quarter_scoped_last_weekday_ordinal() {
let expr = parse("Q3 E1#-1").unwrap(); let utc = Tz::utc();
assert!(expr.covers_in(utc_ms(2026, 9, 28, 12, 0, 0), &utc)); assert!(!expr.covers_in(utc_ms(2026, 9, 21, 12, 0, 0), &utc)); }
#[test]
fn weekly_cadence_windows() {
let expr = parse("20200106/2W/3D").unwrap();
let utc = Tz::utc();
assert!(expr.covers_in(utc_ms(2020, 1, 6, 0, 0, 0), &utc)); assert!(expr.covers_in(utc_ms(2020, 1, 8, 23, 0, 0), &utc)); assert!(!expr.covers_in(utc_ms(2020, 1, 9, 0, 0, 0), &utc)); assert!(!expr.covers_in(utc_ms(2020, 1, 13, 0, 0, 0), &utc)); assert!(expr.covers_in(utc_ms(2020, 1, 20, 12, 0, 0), &utc)); assert!(!expr.covers_in(utc_ms(2020, 1, 5, 0, 0, 0), &utc)); }
#[test]
fn mixed_unit_cadences_parse() {
quiet("20200106/3W/10D"); quiet("20200101T0000/2H/30m"); }
#[test]
fn month_year_duration_requires_a_month_year_period() {
err("20200101/100D/1M");
err("20200101/100D/1Y");
quiet("20200101/2Y/1M"); quiet("20200101/3M/1M"); }
#[test]
fn validate_helper_returns_warnings_and_errors() {
assert!(!validate("M2 Q3").unwrap().is_empty());
assert!(validate("M13").is_err());
}
#[test]
fn covers_resolves_named_zone() {
let expr = parse("M7").unwrap();
assert!(expr.covers(utc_ms(2026, 7, 15, 12, 0, 0), "UTC").unwrap());
assert!(!expr.covers(utc_ms(2026, 3, 15, 12, 0, 0), "UTC").unwrap());
}