use crate::date::Date;
#[must_use]
pub fn fraction(start: Date, end: Date) -> f64 {
if start > end {
return -fraction(end, start);
}
if start == end {
return 0.0;
}
let start_year = start.year();
let end_year = end.year();
if start_year == end_year {
let d = f64::from(start.days_between(end));
let denom = if Date::is_leap_year(start_year) {
366.0_f64
} else {
365.0_f64
};
return d / denom;
}
let mut sum = 0.0_f64;
for y in start_year..=end_year {
let year_start = Date::ymd_unchecked(y, 1, 1);
let year_end = Date::ymd_unchecked(y + 1, 1, 1);
let sub_start = if start > year_start {
start
} else {
year_start
};
let sub_end = if end < year_end { end } else { year_end };
let d_y = sub_start.days_between(sub_end);
if d_y > 0 {
let denom = if Date::is_leap_year(y) {
366.0_f64
} else {
365.0_f64
};
sum += f64::from(d_y) / denom;
}
}
sum
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-12;
#[test]
fn isda_worked_example_2007_to_2008() {
let start = Date::ymd(2007, 12, 28).unwrap();
let end = Date::ymd(2008, 2, 29).unwrap();
let f = fraction(start, end);
let want = 4.0_f64 / 365.0 + 59.0_f64 / 366.0;
assert!(
(f - want).abs() < TOL,
"ISDA worked example: got {f}, want {want}",
);
assert!((f - 0.172_161_089_901_939).abs() < TOL);
}
#[test]
fn same_year_non_leap_simplifies() {
let start = Date::ymd(2026, 3, 1).unwrap();
let end = Date::ymd(2026, 9, 1).unwrap();
assert_eq!(start.days_between(end), 184);
let f = fraction(start, end);
assert!((f - 184.0_f64 / 365.0).abs() < TOL);
}
#[test]
fn same_year_leap_uses_366() {
let start = Date::ymd(2024, 3, 1).unwrap();
let end = Date::ymd(2024, 9, 1).unwrap();
assert_eq!(start.days_between(end), 184);
let f = fraction(start, end);
assert!((f - 184.0_f64 / 366.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(2023, 6, 1).unwrap();
let end = Date::ymd(2025, 6, 1).unwrap();
let forward = fraction(start, end);
let backward = fraction(end, start);
assert!((backward + forward).abs() < TOL);
assert!(backward < 0.0);
}
#[test]
fn additivity_over_split_within_year() {
let a = Date::ymd(2026, 1, 15).unwrap();
let c = Date::ymd(2026, 11, 30).unwrap();
for b in [
Date::ymd(2026, 1, 16).unwrap(),
Date::ymd(2026, 5, 23).unwrap(),
Date::ymd(2026, 11, 29).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}",
);
}
}
#[test]
fn multi_year_span_2023_to_2025() {
let start = Date::ymd(2023, 6, 1).unwrap();
let end = Date::ymd(2025, 6, 1).unwrap();
let f = fraction(start, end);
assert!(
(f - 2.0).abs() < TOL,
"multi-year 2023→2025: got {f}, want 2.0",
);
}
}