#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::collections::BTreeSet;
use fasti::{
Calendar, CalendarBuilder, Date, FixedDate, Month, Rule, Weekday, Weekend, WeekendShift,
calendars,
};
fn shift_of(rule: &Rule) -> WeekendShift {
match rule {
Rule::Fixed(f) => f.weekend_shift(),
_ => WeekendShift::None,
}
}
fn natural(cal: &Calendar<'_>, d: Date) -> bool {
cal.rules.iter().any(|r| r.is_holiday(d))
}
fn mover(cal: &Calendar<'_>, d: Date, pred: impl Fn(WeekendShift) -> bool) -> bool {
cal.is_weekend(d)
&& cal
.rules
.iter()
.any(|r| pred(shift_of(r)) && r.is_holiday(d))
}
fn reference(cal: &Calendar<'_>, lo: Date, hi: Date) -> BTreeSet<Date> {
let mut days = BTreeSet::new();
for serial in lo.serial()..=hi.serial() {
let d = Date::from_serial(serial).unwrap();
if natural(cal, d) {
days.insert(d);
}
}
let spill = Date::from_serial(lo.serial().saturating_sub(7)).unwrap();
let mut sat = spill.next_weekday(Weekday::Sat).unwrap();
while sat.serial() <= hi.serial() {
let sun = sat.add_days(1).unwrap();
if mover(cal, sat, |s| matches!(s, WeekendShift::SatBackSunForward))
&& let Ok(fri) = sat.add_days(-1)
&& !cal.is_weekend(fri)
{
days.insert(fri);
}
let sat_chain = mover(cal, sat, |s| matches!(s, WeekendShift::Forward));
let sun_chain = mover(cal, sun, |s| matches!(s, WeekendShift::Forward));
let sun_single = mover(cal, sun, |s| {
matches!(
s,
WeekendShift::SunForward | WeekendShift::SatBackSunForward
)
});
let mon = sat.add_days(2);
let tue = sat.add_days(3);
let mut insert = |slot: Result<Date, fasti::TimeError>| {
if let Ok(day) = slot
&& !cal.is_weekend(day)
{
days.insert(day);
}
};
let mut mon_taken = mon.is_ok_and(|m| natural(cal, m));
for (chains, single) in [(sat_chain, false), (sun_chain, sun_single)] {
if chains {
if mon_taken {
insert(tue);
} else {
insert(mon);
mon_taken = true;
}
} else if single {
insert(mon);
}
}
sat = match sat.add_days(7) {
Ok(next) => next,
Err(_) => break,
};
}
days
}
fn assert_agrees(cal: &Calendar<'_>, lo: Date, hi: Date) {
let expected = reference(cal, lo, hi);
for serial in lo.serial()..=hi.serial() {
let d = Date::from_serial(serial).unwrap();
assert_eq!(
cal.is_holiday(d),
expected.contains(&d),
"{}: is_holiday({d}) disagrees with the reference model",
cal.name,
);
}
}
macro_rules! model_agreement {
($($test:ident => $cal:expr;)+) => {
$(
#[test]
fn $test() {
assert_agrees(&$cal, Date::MIN, Date::MAX);
}
)+
};
}
model_agreement! {
null_calendar => calendars::NULL_CALENDAR;
weekends_only => calendars::WEEKENDS_ONLY;
target => calendars::TARGET;
france_settlement => calendars::france::SETTLEMENT;
france_exchange => calendars::france::EXCHANGE;
uk_settlement => calendars::uk::SETTLEMENT;
us_settlement => calendars::us::SETTLEMENT;
us_federal_reserve => calendars::us::FEDERAL_RESERVE;
us_government_bond => calendars::us::GOVERNMENT_BOND;
us_sofr => calendars::us::SOFR;
us_nerc => calendars::us::NERC;
us_nyse => calendars::us::NYSE;
}
#[test]
fn synthetic_chaining_calendars_agree_with_the_model() {
let shapes: &[(&str, &[Rule])] = &[
(
"UK Christmas/Boxing Day pair",
&[
Rule::Fixed(FixedDate::new(Month::Dec, 25).shift(WeekendShift::Forward)),
Rule::Fixed(FixedDate::new(Month::Dec, 26).shift(WeekendShift::Forward)),
],
),
(
"chained pair straddling the year boundary",
&[
Rule::Fixed(FixedDate::new(Month::Dec, 31).shift(WeekendShift::Forward)),
Rule::Fixed(FixedDate::new(Month::Jan, 1).shift(WeekendShift::Forward)),
],
),
(
"chain blocked by a natural Monday-capable holiday",
&[
Rule::Fixed(FixedDate::new(Month::Jul, 4).shift(WeekendShift::Forward)),
Rule::Fixed(FixedDate::new(Month::Jul, 5).shift(WeekendShift::Forward)),
Rule::Fixed(FixedDate::new(Month::Jul, 6)),
],
),
(
"mixed variants sharing a weekend",
&[
Rule::Fixed(FixedDate::new(Month::Jul, 4).shift(WeekendShift::Forward)),
Rule::Fixed(FixedDate::new(Month::Jul, 5).shift(WeekendShift::SunForward)),
],
),
(
"single step next to a blocker, plus a backward step",
&[
Rule::Fixed(FixedDate::new(Month::Jan, 1).shift(WeekendShift::SatBackSunForward)),
Rule::Fixed(FixedDate::new(Month::Jan, 2)),
],
),
];
for (name, rules) in shapes {
let cal = Calendar {
name,
weekend: Weekend::SAT_SUN,
rules,
};
assert_agrees(&cal, Date::MIN, Date::MAX);
}
}
mod random_calendars {
use super::*;
use proptest::prelude::*;
fn any_shift() -> impl Strategy<Value = WeekendShift> {
prop_oneof![
Just(WeekendShift::None),
Just(WeekendShift::Forward),
Just(WeekendShift::SunForward),
Just(WeekendShift::SatBackSunForward),
]
}
fn any_fixed_rule() -> impl Strategy<Value = Rule> {
(1u8..=12, 1u8..=31, any_shift()).prop_map(|(m, d, shift)| {
Rule::Fixed(FixedDate::new(Month::try_from_u8(m).unwrap(), d).shift(shift))
})
}
proptest! {
#[test]
fn random_rule_sets_agree_with_the_model(
rules in proptest::collection::vec(any_fixed_rule(), 0..6),
year in 1902u16..=2198,
) {
let mut builder = CalendarBuilder::new("random", Weekend::SAT_SUN);
for rule in rules {
builder = builder.with_rule(rule);
}
let cal = builder.view();
let lo = Date::from_ymd(year - 1, Month::Dec, 20).unwrap();
let hi = Date::from_ymd(year + 1, Month::Jan, 10).unwrap();
let expected = reference(&cal, lo, hi);
for serial in lo.serial()..=hi.serial() {
let d = Date::from_serial(serial).unwrap();
prop_assert_eq!(
cal.is_holiday(d),
expected.contains(&d),
"is_holiday({}) disagrees with the reference model", d,
);
}
}
}
}