use crate::date::Date;
#[must_use]
pub fn fraction(start: Date, end: Date) -> f64 {
f64::from(start.days_between(end)) / 360.0
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-12;
#[test]
fn isda_worked_example_90_days() {
let start = Date::ymd(2026, 1, 1).unwrap();
let end = Date::ymd(2026, 4, 1).unwrap();
let f = fraction(start, end);
assert!((f - 90.0_f64 / 360.0).abs() < 1e-15);
assert!((f - 0.25).abs() < TOL);
}
#[test]
fn one_year_non_leap_is_365_over_360() {
let start = Date::ymd(2026, 2, 1).unwrap();
let end = Date::ymd(2027, 2, 1).unwrap();
let f = fraction(start, end);
assert_eq!(start.days_between(end), 365);
assert!((f - 365.0_f64 / 360.0).abs() < TOL);
}
#[test]
fn zero_length_interval_is_zero() {
let d = Date::ymd(2026, 5, 23).unwrap();
let f = fraction(d, d);
assert!(f.abs() < TOL);
}
#[test]
fn inverted_interval_is_negative() {
let start = Date::ymd(2026, 1, 1).unwrap();
let end = Date::ymd(2026, 4, 1).unwrap();
let forward = fraction(start, end);
let backward = fraction(end, start);
assert!((backward + forward).abs() < TOL);
assert!(backward < 0.0);
}
#[test]
fn one_year_leap_is_366_over_360() {
let start = Date::ymd(2024, 2, 1).unwrap();
let end = Date::ymd(2025, 2, 1).unwrap();
assert_eq!(start.days_between(end), 366);
let f = fraction(start, end);
assert!((f - 366.0_f64 / 360.0).abs() < TOL);
}
#[test]
fn additivity_over_split() {
let a = Date::ymd(2024, 1, 1).unwrap();
let c = Date::ymd(2026, 7, 15).unwrap();
for b in [
Date::ymd(2024, 1, 2).unwrap(),
Date::ymd(2025, 3, 31).unwrap(),
Date::ymd(2026, 7, 14).unwrap(),
] {
let lhs = fraction(a, c);
let rhs = fraction(a, b) + fraction(b, c);
assert!(
(lhs - rhs).abs() < TOL,
"additivity at split {b:?}: lhs={lhs}, rhs={rhs}",
);
}
}
}