#![allow(clippy::doc_markdown)]
use regit_daycount::day_count::{act_360, act_act_isda, thirty_e_360_isda};
use regit_daycount::errors::ValidationError;
use regit_daycount::{Date, DayCount, day_count};
#[cfg(feature = "calendars")]
use regit_daycount::calendar::{self, Composite, JointBusiness};
#[cfg(feature = "calendars")]
use regit_daycount::day_count::bus_252;
#[cfg(feature = "calendars")]
use regit_daycount::{Calendar, Roll, Weekday, roll};
const TOL: f64 = 1e-12;
mod golden {
use super::*;
#[test]
fn isda_act_act_2007_to_2008() {
let start = Date::ymd(2007, 12, 28).unwrap();
let end = Date::ymd(2008, 2, 29).unwrap();
let f = act_act_isda::fraction(start, end);
let want = 4.0_f64 / 365.0 + 59.0_f64 / 366.0;
assert!((f - want).abs() < TOL, "Act/Act ISDA: {f} vs {want}");
assert!((f - 0.172_161_089_901_939).abs() < TOL);
}
#[test]
fn act_360_90_days() {
let start = Date::ymd(2026, 1, 1).unwrap();
let end = Date::ymd(2026, 4, 1).unwrap();
let f = act_360::fraction(start, end);
assert!((f - 0.25).abs() < TOL, "Act/360 90-day: {f}");
}
#[test]
fn act_365f_90_days() {
let start = Date::ymd(2026, 1, 1).unwrap();
let end = Date::ymd(2026, 4, 1).unwrap();
let f = day_count::fraction(start, end, DayCount::Act365F);
assert!((f - 0.246_575_342_465_753).abs() < TOL, "Act/365F: {f}");
}
#[test]
fn thirty_360_6m() {
let start = Date::ymd(2026, 1, 15).unwrap();
let end = Date::ymd(2026, 7, 15).unwrap();
let f = day_count::fraction(start, end, DayCount::Thirty360BondBasis);
assert!((f - 0.5).abs() < TOL, "30/360 6M: {f}");
}
#[test]
fn thirty_e_360_vs_bond_basis_d2_31() {
let start = Date::ymd(2026, 1, 15).unwrap();
let end = Date::ymd(2026, 7, 31).unwrap();
let e_360 = day_count::fraction(start, end, DayCount::ThirtyE360);
let bond = day_count::fraction(start, end, DayCount::Thirty360BondBasis);
assert!((e_360 - 195.0_f64 / 360.0).abs() < TOL, "30E/360: {e_360}");
assert!((bond - 196.0_f64 / 360.0).abs() < TOL, "BondBasis: {bond}");
assert!((bond - e_360 - 1.0_f64 / 360.0).abs() < TOL);
}
#[test]
fn thirty_e_360_isda_maturity_feb_28() {
let start = Date::ymd(2024, 1, 31).unwrap();
let end = Date::ymd(2025, 2, 28).unwrap();
let f = thirty_e_360_isda::fraction(start, end, true);
assert!((f - 388.0_f64 / 360.0).abs() < TOL, "30E/360 ISDA: {f}");
}
#[cfg(feature = "calendars")]
#[test]
fn target2_known_holidays() {
for &(y, m, d) in &[
(2024, 3, 29), (2024, 4, 1), (2024, 12, 25), (2024, 12, 26), ] {
assert!(
calendar::is_holiday(Date::ymd(y, m, d).unwrap(), Calendar::Target2),
"TARGET2 holiday {y}-{m}-{d}",
);
}
}
#[cfg(feature = "calendars")]
#[test]
fn us_known_holidays() {
for &(y, m, d) in &[
(2024, 7, 4), (2024, 11, 28), ] {
assert!(
calendar::is_holiday(Date::ymd(y, m, d).unwrap(), Calendar::UnitedStates),
"US holiday {y}-{m}-{d}",
);
}
}
#[cfg(feature = "calendars")]
#[test]
fn uk_platinum_jubilee_2022() {
assert!(calendar::is_holiday(
Date::ymd(2022, 6, 2).unwrap(),
Calendar::UnitedKingdom,
));
assert!(calendar::is_holiday(
Date::ymd(2022, 6, 3).unwrap(),
Calendar::UnitedKingdom,
));
}
#[cfg(feature = "calendars")]
#[test]
fn joint_us_eur_4_july_not_business() {
static LEGS: &[Calendar] = &[Calendar::Target2, Calendar::UnitedStates];
let jb = JointBusiness::new(LEGS);
assert!(!jb.is_business_day(Date::ymd(2024, 7, 4).unwrap()));
assert!(jb.is_holiday(Date::ymd(2024, 7, 4).unwrap()));
}
#[cfg(feature = "calendars")]
#[test]
fn bus_252_target2_two_weeks() {
let s = Date::ymd(2026, 5, 1).unwrap();
let e = Date::ymd(2026, 5, 15).unwrap();
let f = calendar::bus_252_for_calendar(s, e, Calendar::Target2);
assert!((f - 9.0_f64 / 252.0).abs() < TOL, "Bus/252 TARGET2: {f}");
}
}
mod invalid {
use super::*;
#[test]
fn ymd_rejects_february_29_in_non_leap() {
assert_eq!(
Date::ymd(2025, 2, 29),
Err(ValidationError::InvalidDate {
rule: "day-out-of-range",
}),
);
}
#[test]
fn ymd_rejects_year_below_1583() {
assert_eq!(
Date::ymd(1582, 1, 1),
Err(ValidationError::OutOfRange {
what: "year < 1583",
}),
);
}
#[test]
fn ymd_rejects_month_zero_and_thirteen() {
assert_eq!(
Date::ymd(2026, 0, 1),
Err(ValidationError::InvalidDate {
rule: "month-out-of-range",
}),
);
assert_eq!(
Date::ymd(2026, 13, 1),
Err(ValidationError::InvalidDate {
rule: "month-out-of-range",
}),
);
}
#[test]
fn dispatcher_returns_nan_for_act_act_icma_without_freq() {
let s = Date::ymd(2026, 3, 1).unwrap();
let e = Date::ymd(2026, 9, 1).unwrap();
assert!(day_count::fraction(s, e, DayCount::ActActIcma).is_nan());
}
#[test]
fn dispatcher_returns_nan_for_bus_252_without_predicate() {
let s = Date::ymd(2026, 5, 1).unwrap();
let e = Date::ymd(2026, 5, 15).unwrap();
assert!(day_count::fraction(s, e, DayCount::Bus252).is_nan());
assert!(day_count::year_fraction_with_freq(s, e, DayCount::Bus252, 1, s, e).is_nan(),);
}
#[cfg(feature = "calendars")]
#[test]
fn out_of_coverage_calendar_returns_false_not_panic() {
assert!(!calendar::is_holiday(
Date::ymd(2019, 12, 25).unwrap(),
Calendar::UnitedStates,
));
}
}
mod roundtrip {
use super::*;
#[test]
fn ymd_roundtrip_for_named_dates() {
for &(y, m, d) in &[
(1969, 7, 20), (1980, 12, 12), (1999, 12, 31), (2000, 1, 1), (2007, 12, 28), (2008, 2, 29), (2020, 2, 29), (2024, 2, 29), (2026, 5, 23), (2038, 1, 19), (9999, 12, 31), ] {
let d0 = Date::ymd(y, m, d).expect("named date should parse");
let d1 = Date::ymd(d0.year(), d0.month(), d0.day());
assert_eq!(d1, Ok(d0), "round-trip {y}-{m}-{d}");
}
}
#[test]
fn add_days_round_trips() {
let anchors = [
Date::ymd(2026, 5, 23).unwrap(),
Date::ymd(2024, 2, 29).unwrap(),
];
for &d in &anchors {
for &n in &[-1000_i32, -1, 0, 1, 1000] {
assert_eq!(d.add_days(n).add_days(-n), d, "anchor {d:?} offset {n}");
}
}
}
#[test]
fn day_of_week_is_deterministic() {
let mut d = Date::ymd(2024, 2, 25).unwrap();
for _ in 0..10 {
assert_eq!(d.day_of_week(), d.day_of_week(), "stable for {d:?}");
d = d.add_days(1);
}
}
}
mod properties {
use super::*;
use proptest::prelude::*;
prop_compose! {
fn arb_date()
(year in 1583i32..=9999, doy in 0u32..366)
-> Date
{
let base = Date::ymd_unchecked(year, 1, 1);
let cap = if year == 9999 { 364 } else { 365 };
let n = core::cmp::min(doy, cap);
#[allow(clippy::cast_possible_wrap)]
let n_i32 = n as i32;
base.add_days(n_i32)
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 256,
..ProptestConfig::default()
})]
#[test]
fn prop_fraction_is_zero_on_zero_length(d in arb_date()) {
for &basis in &[
DayCount::Act360,
DayCount::Act365F,
DayCount::ActActIsda,
DayCount::Thirty360BondBasis,
DayCount::ThirtyE360,
DayCount::ThirtyE360Isda,
DayCount::Act365L,
DayCount::Nl365,
] {
let f = day_count::fraction(d, d, basis);
prop_assert!(f.abs() < TOL, "basis {basis:?}: {f}");
}
}
#[test]
fn prop_act_360_is_additive(
a in arb_date(),
n1 in 0i32..3_650,
n2 in 0i32..3_650,
) {
let b = a.add_days(n1);
let c = b.add_days(n2);
let lhs = day_count::fraction(a, c, DayCount::Act360);
let rhs = day_count::fraction(a, b, DayCount::Act360)
+ day_count::fraction(b, c, DayCount::Act360);
prop_assert!((lhs - rhs).abs() < TOL, "lhs={lhs}, rhs={rhs}");
}
#[test]
fn prop_add_days_round_trip(
d in arb_date(),
n in -10_000i32..10_000,
) {
prop_assert_eq!(d.add_days(n).add_days(-n), d);
}
}
#[cfg(feature = "calendars")]
proptest! {
#![proptest_config(ProptestConfig {
cases: 256,
..ProptestConfig::default()
})]
#[test]
fn prop_is_holiday_is_deterministic(d in arb_date()) {
for &cal in &[
Calendar::Target2,
Calendar::UnitedStates,
Calendar::UnitedKingdom,
Calendar::Japan,
Calendar::Switzerland,
Calendar::HongKong,
Calendar::Singapore,
] {
prop_assert_eq!(
calendar::is_holiday(d, cal),
calendar::is_holiday(d, cal),
);
}
}
#[test]
fn prop_is_weekend_xor_business_day_or_holiday(d in arb_date()) {
for &cal in &[
Calendar::Target2,
Calendar::UnitedStates,
Calendar::UnitedKingdom,
Calendar::Japan,
Calendar::Switzerland,
Calendar::HongKong,
Calendar::Singapore,
] {
let wknd = calendar::is_weekend(d);
let hol = calendar::is_holiday(d, cal);
let biz = calendar::is_business_day(d, cal);
prop_assert!(
biz != (wknd || hol),
"biz/non-biz partition fails at {:?} ({:?})", d, cal,
);
prop_assert_eq!(biz, !wknd && !hol);
}
}
}
#[test]
fn one_one_is_one_on_zero_length() {
let d = Date::ymd(2026, 5, 23).unwrap();
assert!((day_count::fraction(d, d, DayCount::OneOne) - 1.0).abs() < TOL);
}
}
mod cross_oracle {
use super::*;
const QL_TOL: f64 = 1e-10;
#[test]
fn quantlib_act_act_isda_2003_11_01_to_2004_05_01() {
let s = Date::ymd(2003, 11, 1).unwrap();
let e = Date::ymd(2004, 5, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::ActActIsda);
assert!((f - 0.497_724_380_567).abs() < TOL, "{f}");
}
#[test]
fn quantlib_act_act_isda_1999_02_01_to_1999_07_01() {
let s = Date::ymd(1999, 2, 1).unwrap();
let e = Date::ymd(1999, 7, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::ActActIsda);
assert!((f - 0.410_958_904_110).abs() < TOL, "{f}");
}
#[test]
fn quantlib_act_act_isda_1999_07_01_to_2000_07_01() {
let s = Date::ymd(1999, 7, 1).unwrap();
let e = Date::ymd(2000, 7, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::ActActIsda);
let exact = 184.0_f64 / 365.0 + 182.0_f64 / 366.0;
assert!((f - exact).abs() < TOL, "{f}");
assert!((f - 1.001_377_348_600).abs() < QL_TOL, "{f}");
}
#[test]
fn quantlib_act_act_isda_2002_08_15_to_2003_07_15() {
let s = Date::ymd(2002, 8, 15).unwrap();
let e = Date::ymd(2003, 7, 15).unwrap();
let f = day_count::fraction(s, e, DayCount::ActActIsda);
assert!((f - 0.915_068_493_151).abs() < TOL, "{f}");
}
#[test]
fn quantlib_act_act_isda_2003_07_15_to_2004_01_15() {
let s = Date::ymd(2003, 7, 15).unwrap();
let e = Date::ymd(2004, 1, 15).unwrap();
let f = day_count::fraction(s, e, DayCount::ActActIsda);
assert!((f - 0.504_004_790_778).abs() < TOL, "{f}");
}
#[test]
fn quantlib_act_act_isda_2000_01_30_to_2000_06_30() {
let s = Date::ymd(2000, 1, 30).unwrap();
let e = Date::ymd(2000, 6, 30).unwrap();
let f = day_count::fraction(s, e, DayCount::ActActIsda);
assert!((f - 0.415_300_546_448).abs() < TOL, "{f}");
}
#[test]
fn quantlib_act_act_icma_1999_02_01_to_1999_07_01_annual_ref() {
let s = Date::ymd(1999, 2, 1).unwrap();
let e = Date::ymd(1999, 7, 1).unwrap();
let ref_start = Date::ymd(1999, 1, 1).unwrap();
let ref_end = Date::ymd(2000, 1, 1).unwrap();
let f =
day_count::year_fraction_with_freq(s, e, DayCount::ActActIcma, 1, ref_start, ref_end);
assert!((f - 0.410_958_904_110).abs() < TOL, "{f}");
}
#[test]
fn quantlib_act_act_icma_full_semiannual_period_is_half() {
let s = Date::ymd(2003, 11, 1).unwrap();
let e = Date::ymd(2004, 5, 1).unwrap();
let f = day_count::year_fraction_with_freq(s, e, DayCount::ActActIcma, 2, s, e);
assert!((f - 0.500_000_000_000).abs() < TOL, "{f}");
}
#[test]
fn quantlib_act_act_icma_full_annual_period_is_one() {
let s = Date::ymd(1999, 7, 1).unwrap();
let e = Date::ymd(2000, 7, 1).unwrap();
let f = day_count::year_fraction_with_freq(s, e, DayCount::ActActIcma, 1, s, e);
assert!((f - 1.000_000_000_000).abs() < TOL, "{f}");
}
#[test]
fn quantlib_bond_basis_2006_08_31_to_2007_02_28() {
let s = Date::ymd(2006, 8, 31).unwrap();
let e = Date::ymd(2007, 2, 28).unwrap();
let f = day_count::fraction(s, e, DayCount::Thirty360BondBasis);
assert!((f - 178.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_bond_basis_2007_02_28_to_2007_08_31() {
let s = Date::ymd(2007, 2, 28).unwrap();
let e = Date::ymd(2007, 8, 31).unwrap();
let f = day_count::fraction(s, e, DayCount::Thirty360BondBasis);
assert!((f - 183.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_bond_basis_2007_08_31_to_2008_02_29() {
let s = Date::ymd(2007, 8, 31).unwrap();
let e = Date::ymd(2008, 2, 29).unwrap();
let f = day_count::fraction(s, e, DayCount::Thirty360BondBasis);
assert!((f - 179.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_bond_basis_2008_02_28_to_2008_08_31() {
let s = Date::ymd(2008, 2, 28).unwrap();
let e = Date::ymd(2008, 8, 31).unwrap();
let f = day_count::fraction(s, e, DayCount::Thirty360BondBasis);
assert!((f - 183.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_bond_basis_2008_02_29_to_2009_02_28() {
let s = Date::ymd(2008, 2, 29).unwrap();
let e = Date::ymd(2009, 2, 28).unwrap();
let f = day_count::fraction(s, e, DayCount::Thirty360BondBasis);
assert!((f - 359.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_bond_basis_2008_02_28_to_2008_03_31() {
let s = Date::ymd(2008, 2, 28).unwrap();
let e = Date::ymd(2008, 3, 31).unwrap();
let f = day_count::fraction(s, e, DayCount::Thirty360BondBasis);
assert!((f - 33.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_eurobond_2006_02_28_to_2006_08_31() {
let s = Date::ymd(2006, 2, 28).unwrap();
let e = Date::ymd(2006, 8, 31).unwrap();
let f = day_count::fraction(s, e, DayCount::ThirtyE360);
assert!((f - 182.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_eurobond_2007_02_28_to_2007_08_31() {
let s = Date::ymd(2007, 2, 28).unwrap();
let e = Date::ymd(2007, 8, 31).unwrap();
let f = day_count::fraction(s, e, DayCount::ThirtyE360);
assert!((f - 182.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_eurobond_2008_02_29_to_2008_08_31() {
let s = Date::ymd(2008, 2, 29).unwrap();
let e = Date::ymd(2008, 8, 31).unwrap();
let f = day_count::fraction(s, e, DayCount::ThirtyE360);
assert!((f - 181.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_eurobond_2008_02_28_to_2008_08_31() {
let s = Date::ymd(2008, 2, 28).unwrap();
let e = Date::ymd(2008, 8, 31).unwrap();
let f = day_count::fraction(s, e, DayCount::ThirtyE360);
assert!((f - 182.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_eurobond_2008_02_28_to_2008_03_31() {
let s = Date::ymd(2008, 2, 28).unwrap();
let e = Date::ymd(2008, 3, 31).unwrap();
let f = day_count::fraction(s, e, DayCount::ThirtyE360);
assert!((f - 32.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_eurobond_2008_02_29_to_2009_02_28() {
let s = Date::ymd(2008, 2, 29).unwrap();
let e = Date::ymd(2009, 2, 28).unwrap();
let f = day_count::fraction(s, e, DayCount::ThirtyE360);
assert!((f - 359.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_eurobond_isda_2007_08_31_to_2008_02_29_not_maturity() {
let s = Date::ymd(2007, 8, 31).unwrap();
let e = Date::ymd(2008, 2, 29).unwrap();
let f = thirty_e_360_isda::fraction(s, e, false);
assert!((f - 180.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_eurobond_isda_2011_08_31_to_2012_02_29_at_maturity() {
let s = Date::ymd(2011, 8, 31).unwrap();
let e = Date::ymd(2012, 2, 29).unwrap();
let f = thirty_e_360_isda::fraction(s, e, true);
assert!((f - 179.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_eurobond_isda_2006_01_31_to_2006_02_28() {
let s = Date::ymd(2006, 1, 31).unwrap();
let e = Date::ymd(2006, 2, 28).unwrap();
let f = thirty_e_360_isda::fraction(s, e, false);
assert!((f - 30.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_eurobond_isda_2007_08_31_to_2008_02_28_not_last_day() {
let s = Date::ymd(2007, 8, 31).unwrap();
let e = Date::ymd(2008, 2, 28).unwrap();
let f = thirty_e_360_isda::fraction(s, e, false);
assert!((f - 178.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_eurobond_isda_2007_02_28_to_2008_02_29_at_maturity() {
let s = Date::ymd(2007, 2, 28).unwrap();
let e = Date::ymd(2008, 2, 29).unwrap();
let f = thirty_e_360_isda::fraction(s, e, true);
assert!((f - 359.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn quantlib_eurobond_isda_2008_02_29_to_2009_02_28_not_maturity() {
let s = Date::ymd(2008, 2, 29).unwrap();
let e = Date::ymd(2009, 2, 28).unwrap();
let f = thirty_e_360_isda::fraction(s, e, false);
assert!((f - 360.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn hand_act_360_four_year_span_includes_leap() {
let s = Date::ymd(2020, 3, 1).unwrap();
let e = Date::ymd(2024, 3, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::Act360);
assert!((f - 1461.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn hand_act_360_eighteen_months_non_leap() {
let s = Date::ymd(2025, 1, 1).unwrap();
let e = Date::ymd(2026, 7, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::Act360);
assert!((f - 546.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn hand_act_360_february_non_leap() {
let s = Date::ymd(2025, 2, 1).unwrap();
let e = Date::ymd(2025, 3, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::Act360);
assert!((f - 28.0_f64 / 360.0).abs() < TOL, "{f}");
}
#[test]
fn hand_act_365f_four_year_span_includes_leap() {
let s = Date::ymd(2020, 3, 1).unwrap();
let e = Date::ymd(2024, 3, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::Act365F);
assert!((f - 1461.0_f64 / 365.0).abs() < TOL, "{f}");
}
#[test]
fn hand_act_365f_third_quarter_non_leap() {
let s = Date::ymd(2025, 7, 1).unwrap();
let e = Date::ymd(2025, 10, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::Act365F);
assert!((f - 92.0_f64 / 365.0).abs() < TOL, "{f}");
}
#[test]
fn hand_act_365f_leap_day_on_start_to_28_feb_next_year() {
let s = Date::ymd(2024, 2, 29).unwrap();
let e = Date::ymd(2025, 2, 28).unwrap();
let f = day_count::fraction(s, e, DayCount::Act365F);
assert!((f - 1.0).abs() < TOL, "{f}");
}
#[test]
fn hand_act_365l_full_non_leap_year() {
let s = Date::ymd(2025, 1, 1).unwrap();
let e = Date::ymd(2026, 1, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::Act365L);
assert!((f - 1.0).abs() < TOL, "{f}");
}
#[test]
fn hand_act_365l_q3_2024_after_leap() {
let s = Date::ymd(2024, 7, 1).unwrap();
let e = Date::ymd(2024, 10, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::Act365L);
assert!((f - 92.0_f64 / 365.0).abs() < TOL, "{f}");
}
#[test]
fn hand_act_365l_straddle_leap_day_2024() {
let s = Date::ymd(2023, 12, 1).unwrap();
let e = Date::ymd(2024, 6, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::Act365L);
assert!((f - 183.0_f64 / 366.0).abs() < TOL, "{f}");
assert!((f - 0.5).abs() < TOL, "{f}");
}
#[test]
fn hand_nl_365_full_leap_year_is_one() {
let s = Date::ymd(2024, 1, 1).unwrap();
let e = Date::ymd(2025, 1, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::Nl365);
assert!((f - 1.0).abs() < TOL, "{f}");
}
#[test]
fn hand_nl_365_eight_year_span_two_leaps() {
let s = Date::ymd(2020, 1, 1).unwrap();
let e = Date::ymd(2028, 1, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::Nl365);
assert!((f - 8.0).abs() < TOL, "{f}");
}
#[test]
fn hand_nl_365_q1_non_leap() {
let s = Date::ymd(2025, 1, 1).unwrap();
let e = Date::ymd(2025, 4, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::Nl365);
assert!((f - 90.0_f64 / 365.0).abs() < TOL, "{f}");
}
#[test]
fn hand_act_act_isda_full_non_leap_year() {
let s = Date::ymd(2025, 1, 1).unwrap();
let e = Date::ymd(2026, 1, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::ActActIsda);
assert!((f - 1.0).abs() < TOL, "{f}");
}
#[test]
fn hand_act_act_isda_full_leap_year() {
let s = Date::ymd(2024, 1, 1).unwrap();
let e = Date::ymd(2025, 1, 1).unwrap();
let f = day_count::fraction(s, e, DayCount::ActActIsda);
assert!((f - 1.0).abs() < TOL, "{f}");
}
#[cfg(feature = "calendars")]
#[test]
fn hand_bus_252_one_business_week_weekends_only() {
use regit_daycount::day_count::bus_252;
let s = Date::ymd(2026, 1, 5).unwrap();
let e = Date::ymd(2026, 1, 12).unwrap();
let f = bus_252::fraction(s, e, |d| !calendar::is_weekend(d));
assert!((f - 5.0_f64 / 252.0).abs() < TOL, "{f}");
}
#[cfg(feature = "calendars")]
#[test]
fn hand_bus_252_three_business_weeks_weekends_only() {
use regit_daycount::day_count::bus_252;
let s = Date::ymd(2026, 1, 5).unwrap();
let e = Date::ymd(2026, 1, 26).unwrap();
let f = bus_252::fraction(s, e, |d| !calendar::is_weekend(d));
assert!((f - 15.0_f64 / 252.0).abs() < TOL, "{f}");
}
#[cfg(feature = "calendars")]
#[test]
fn hand_bus_252_target2_christmas_week_2024() {
let s = Date::ymd(2024, 12, 23).unwrap();
let e = Date::ymd(2024, 12, 30).unwrap();
let f = calendar::bus_252_for_calendar(s, e, Calendar::Target2);
assert!((f - 3.0_f64 / 252.0).abs() < TOL, "{f}");
}
}
#[cfg(feature = "calendars")]
mod cross_module {
use super::*;
#[test]
fn adjust_then_fraction_under_target2() {
let saturday = Date::ymd(2026, 5, 23).unwrap();
assert_eq!(saturday.day_of_week(), Weekday::Sat);
let adjusted = calendar::adjust(saturday, Roll::Following, Calendar::Target2);
assert_eq!(adjusted, Date::ymd(2026, 5, 25).unwrap());
let start = Date::ymd(2026, 5, 1).unwrap();
let f = day_count::fraction(start, adjusted, DayCount::Act360);
assert!((f - 24.0_f64 / 360.0).abs() < TOL, "Act/360 1→25 May: {f}");
}
#[test]
fn composite_then_business_days_between() {
static REGIONS: &[Calendar] = &[Calendar::Target2, Calendar::UnitedStates];
let comp = Composite::new(REGIONS);
let start = Date::ymd(2024, 7, 1).unwrap();
let end = Date::ymd(2024, 7, 8).unwrap();
let mut count = 0u32;
let mut d = start;
while d < end {
if comp.is_business_day(d) {
count += 1;
}
d = d.add_days(1);
}
assert_eq!(count, 4, "composite business days in week of 4 Jul 2024");
assert!(comp.is_business_day(Date::ymd(2024, 7, 1).unwrap())); assert!(comp.is_business_day(Date::ymd(2024, 7, 2).unwrap())); assert!(comp.is_business_day(Date::ymd(2024, 7, 3).unwrap())); assert!(!comp.is_business_day(Date::ymd(2024, 7, 4).unwrap())); assert!(comp.is_business_day(Date::ymd(2024, 7, 5).unwrap())); assert!(!comp.is_business_day(Date::ymd(2024, 7, 6).unwrap())); assert!(!comp.is_business_day(Date::ymd(2024, 7, 7).unwrap())); }
#[test]
fn bus_252_for_calendar_matches_direct_bus_252_call() {
let s = Date::ymd(2026, 5, 1).unwrap();
let e = Date::ymd(2026, 5, 15).unwrap();
let via_helper = calendar::bus_252_for_calendar(s, e, Calendar::Target2);
let via_direct =
bus_252::fraction(s, e, |d| calendar::is_business_day(d, Calendar::Target2));
assert_eq!(
via_helper.to_bits(),
via_direct.to_bits(),
"{via_helper} vs {via_direct}",
);
}
#[test]
fn roll_apply_matches_calendar_adjust() {
let saturday = Date::ymd(2026, 5, 23).unwrap();
let via_roll = roll::apply(saturday, Roll::Following, |d| {
calendar::is_business_day(d, Calendar::Target2)
});
let via_cal = calendar::adjust(saturday, Roll::Following, Calendar::Target2);
assert_eq!(via_roll, via_cal);
}
}