use crate::date::Date;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DayCount {
Act360,
Act365F,
ActActIsda,
ActActIcma,
Thirty360BondBasis,
ThirtyE360,
ThirtyE360Isda,
Act365L,
Nl365,
Bus252,
OneOne,
}
pub mod act_360;
pub mod act_365f;
pub mod act_365l;
pub mod act_act_icma;
pub mod act_act_isda;
pub mod bus_252;
pub mod nl_365;
pub mod one_one;
pub mod thirty_360_bond_basis;
pub mod thirty_e_360;
pub mod thirty_e_360_isda;
#[must_use]
pub fn fraction(start: Date, end: Date, basis: DayCount) -> f64 {
match basis {
DayCount::Act360 => act_360::fraction(start, end),
DayCount::Act365F => act_365f::fraction(start, end),
DayCount::ActActIsda => act_act_isda::fraction(start, end),
DayCount::Thirty360BondBasis => thirty_360_bond_basis::fraction(start, end),
DayCount::ThirtyE360 => thirty_e_360::fraction(start, end),
DayCount::ThirtyE360Isda => thirty_e_360_isda::fraction(start, end, false),
DayCount::Act365L => act_365l::fraction(start, end),
DayCount::Nl365 => nl_365::fraction(start, end),
DayCount::OneOne => one_one::fraction(start, end),
DayCount::ActActIcma | DayCount::Bus252 => f64::NAN,
}
}
#[must_use]
pub fn year_fraction_with_freq(
start: Date,
end: Date,
basis: DayCount,
freq: u8,
ref_start: Date,
ref_end: Date,
) -> f64 {
match basis {
DayCount::ActActIcma => act_act_icma::fraction(start, end, ref_start, ref_end, freq),
_ => fraction(start, end, basis),
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-12;
#[test]
fn dispatcher_routes_act_360() {
let s = Date::ymd(2026, 1, 1).unwrap();
let e = Date::ymd(2026, 4, 1).unwrap();
assert!((fraction(s, e, DayCount::Act360) - 90.0 / 360.0).abs() < TOL);
}
#[test]
fn dispatcher_routes_act_365f() {
let s = Date::ymd(2026, 1, 1).unwrap();
let e = Date::ymd(2026, 4, 1).unwrap();
assert!((fraction(s, e, DayCount::Act365F) - 90.0 / 365.0).abs() < TOL);
}
#[test]
fn dispatcher_routes_act_act_isda() {
let s = Date::ymd(2007, 12, 28).unwrap();
let e = Date::ymd(2008, 2, 29).unwrap();
let expected = 4.0 / 365.0 + 59.0 / 366.0;
assert!((fraction(s, e, DayCount::ActActIsda) - expected).abs() < TOL);
}
#[test]
fn dispatcher_routes_thirty_360_bond_basis() {
let s = Date::ymd(2026, 1, 15).unwrap();
let e = Date::ymd(2026, 7, 15).unwrap();
assert!((fraction(s, e, DayCount::Thirty360BondBasis) - 0.5).abs() < TOL);
}
#[test]
fn dispatcher_routes_thirty_e_360() {
let s = Date::ymd(2026, 1, 15).unwrap();
let e = Date::ymd(2026, 7, 31).unwrap();
assert!((fraction(s, e, DayCount::ThirtyE360) - 195.0 / 360.0).abs() < TOL);
}
#[test]
fn dispatcher_routes_thirty_e_360_isda_with_no_maturity_default() {
let s = Date::ymd(2024, 2, 29).unwrap();
let e = Date::ymd(2024, 8, 31).unwrap();
assert!((fraction(s, e, DayCount::ThirtyE360Isda) - 0.5).abs() < TOL);
}
#[test]
fn dispatcher_routes_act_365l() {
let s = Date::ymd(2024, 1, 1).unwrap();
let e = Date::ymd(2024, 4, 1).unwrap();
assert!((fraction(s, e, DayCount::Act365L) - 91.0 / 366.0).abs() < TOL);
}
#[test]
fn dispatcher_routes_nl_365() {
let s = Date::ymd(2024, 1, 1).unwrap();
let e = Date::ymd(2024, 4, 1).unwrap();
assert!((fraction(s, e, DayCount::Nl365) - 90.0 / 365.0).abs() < TOL);
}
#[test]
fn dispatcher_routes_one_one() {
let s = Date::ymd(2026, 1, 1).unwrap();
let e = Date::ymd(2026, 4, 1).unwrap();
assert!((fraction(s, e, DayCount::OneOne) - 1.0).abs() < TOL);
}
#[test]
fn dispatcher_returns_nan_for_act_act_icma() {
let s = Date::ymd(2026, 3, 1).unwrap();
let e = Date::ymd(2026, 9, 1).unwrap();
assert!(fraction(s, e, DayCount::ActActIcma).is_nan());
}
#[test]
fn dispatcher_returns_nan_for_bus_252() {
let s = Date::ymd(2026, 5, 1).unwrap();
let e = Date::ymd(2026, 5, 15).unwrap();
assert!(fraction(s, e, DayCount::Bus252).is_nan());
}
#[test]
fn freq_dispatcher_routes_act_act_icma() {
let s = Date::ymd(2026, 3, 1).unwrap();
let e = Date::ymd(2026, 9, 1).unwrap();
let yf = year_fraction_with_freq(s, e, DayCount::ActActIcma, 2, s, e);
assert!((yf - 0.5).abs() < TOL);
}
#[test]
fn freq_dispatcher_ignores_ref_args_for_other_variants() {
let s = Date::ymd(2026, 1, 1).unwrap();
let e = Date::ymd(2026, 4, 1).unwrap();
let with_freq = year_fraction_with_freq(s, e, DayCount::Act360, 99, s, e);
let direct = fraction(s, e, DayCount::Act360);
assert!((with_freq - direct).abs() < TOL);
}
#[test]
fn freq_dispatcher_still_nan_for_bus_252() {
let s = Date::ymd(2026, 5, 1).unwrap();
let e = Date::ymd(2026, 5, 15).unwrap();
let yf = year_fraction_with_freq(s, e, DayCount::Bus252, 1, s, e);
assert!(yf.is_nan());
}
}