use dtrexp::{parse, 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 guard_rejections_carry_their_own_message() {
err_msg("M:5", "designator requires a value");
err_msg("T!0900", "T takes no exclusion — write the complement explicitly");
err_msg("T*", "T takes no '*'");
err_msg("T", "T requires a value");
}
#[test]
fn time_value_upper_bounds_are_inclusive() {
quiet("T23"); quiet("T2359"); quiet("T235959"); err("T2360"); }
#[test]
fn four_digit_time_value_weights_its_minutes() {
let expr = parse("T0130").unwrap();
let utc = Tz::utc();
assert!(expr.covers_in(utc_ms(2020, 1, 1, 1, 30, 30), &utc));
assert!(!expr.covers_in(utc_ms(2020, 1, 1, 1, 0, 0), &utc));
}
#[test]
fn millisecond_time_value_weights_its_digits() {
let expr = parse("T120000.015").unwrap();
let utc = Tz::utc();
let noon = utc_ms(2020, 1, 1, 12, 0, 0);
assert!(expr.covers_in(noon + 15, &utc));
assert!(!expr.covers_in(noon + 5, &utc));
}
#[test]
fn date_literal_requires_eight_digits_not_eight_bytes() {
err("2020010A"); }
#[test]
fn date_literal_year_digit_weights() {
let utc = Tz::utc();
let expr = parse("19900101:*").unwrap();
assert!(!expr.covers_in(utc_ms(1989, 6, 1, 0, 0, 0), &utc));
assert!(expr.covers_in(utc_ms(1991, 6, 1, 0, 0, 0), &utc));
let expr = parse("20200101:*").unwrap();
assert!(!expr.covers_in(utc_ms(2010, 6, 1, 0, 0, 0), &utc));
}
#[test]
fn date_literal_validity_checks_each_field() {
err("20200230"); err("20201301"); err("20200100"); }
#[test]
fn date_literal_time_part_boundaries() {
quiet("20200101T2300:*"); err("20200101T2430:*"); quiet("20200101T2359:*"); err("20200101T1360:*"); }
#[test]
fn date_literal_seconds_weight() {
let expr = parse("20200101T120059").unwrap();
let utc = Tz::utc();
assert!(expr.covers_in(utc_ms(2020, 1, 1, 12, 0, 59), &utc));
assert!(!expr.covers_in(utc_ms(2020, 1, 1, 12, 0, 50), &utc));
}
#[test]
fn bounds_start_honours_seconds() {
let expr = parse("20200101T120030:*").unwrap();
let utc = Tz::utc();
assert!(!expr.covers_in(utc_ms(2020, 1, 1, 12, 0, 15), &utc));
assert!(expr.covers_in(utc_ms(2020, 1, 1, 12, 0, 30), &utc)); assert!(expr.covers_in(utc_ms(2020, 1, 1, 12, 0, 45), &utc));
}
#[test]
fn a_degenerate_range_takes_a_stride() {
quiet("M3:3/2");
}
#[test]
fn read_uint_accepts_exactly_ten_million() {
quiet("20200101/10000000m");
}
#[test]
fn cadence_length_comparison_uses_exact_unit_lengths() {
err("20200101/25M/2Y"); err("20200101/1Y/12M"); err("20200101/1M/4W"); err("20200101/1M/28D"); quiet("20200101/1M/27D"); }
#[test]
fn the_seventh_can_be_the_first_weekday_of_its_kind() {
let expr = parse("E1#1").unwrap();
let utc = Tz::utc();
assert!(expr.covers_in(utc_ms(2020, 9, 7, 12, 0, 0), &utc));
assert!(!expr.covers_in(utc_ms(2020, 9, 14, 12, 0, 0), &utc)); }
#[test]
fn absolute_cadence_window_edges() {
let expr = parse("20200601T0000/2H/1H").unwrap();
let utc = Tz::utc();
let anchor = utc_ms(2020, 6, 1, 0, 0, 0);
assert!(expr.covers_in(anchor, &utc)); assert!(!expr.covers_in(anchor + 3_600_000, &utc)); assert!(!expr.covers_in(anchor - 7_200_000, &utc)); assert!(expr.covers_in(anchor + 7_200_000, &utc)); }
#[test]
fn monthly_cadence_with_a_mid_year_anchor() {
let expr = parse("20200615/2M").unwrap();
let utc = Tz::utc();
assert!(expr.covers_in(utc_ms(2020, 8, 20, 12, 0, 0), &utc)); assert!(!expr.covers_in(utc_ms(2020, 7, 20, 12, 0, 0), &utc)); }
#[test]
fn stride_interval_lower_and_upper_edges_are_legal() {
quiet("Y2000:3000/2"); quiet("M1/12"); }
#[test]
fn enumerated_year_ranges_detect_unsatisfiable_days() {
warns("Y2001:2003 M2 D29");
}
#[test]
fn the_year_span_cutoff_is_strictly_over_1000() {
warns("Y1001,2001 M2 D29");
quiet("Y1001,2002 M2 D29");
}
#[test]
fn ordinal_weekday_selectors_never_warn() {
quiet("E1#2");
}
#[test]
fn exclude_everything_on_weekday_warns() {
warns("E!*");
}
#[test]
fn quarter_mapping_uses_month_minus_one() {
quiet("M3 Q1"); }
#[test]
fn disjoint_warning_points_at_the_month_selector() {
let p = parse("Q3 M2").unwrap();
let w = p.warnings();
assert_eq!(w.len(), 1);
assert_eq!(w[0].pos, 3, "the warning should anchor to M, not Q");
}