use crate::date::Date;
#[must_use]
pub fn fraction(start: Date, end: Date) -> f64 {
let numerator = f64::from(start.days_between(end));
let denominator = if period_contains_leap_day(start, end) {
366.0_f64
} else {
365.0_f64
};
numerator / denominator
}
fn period_contains_leap_day(start: Date, end: Date) -> bool {
let (lower, upper) = if start <= end {
(start, end)
} else {
(end, start)
};
let y_lo = lower.year();
let y_hi = upper.year();
let mut y = y_lo;
while y <= y_hi {
if Date::is_leap_year(y) {
let feb29 = Date::ymd_unchecked(y, 2, 29);
if lower <= feb29 && feb29 < upper {
return true;
}
}
y += 1;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-12;
#[test]
fn worked_2024_q1_straddles_leap() {
let start = Date::ymd(2024, 1, 1).unwrap();
let end = Date::ymd(2024, 4, 1).unwrap();
assert_eq!(start.days_between(end), 91);
let f = fraction(start, end);
assert!((f - 91.0_f64 / 366.0).abs() < 1e-15);
assert!((f - 0.248_633_879_781_421).abs() < TOL);
}
#[test]
fn worked_2024_q2_no_leap() {
let start = Date::ymd(2024, 4, 1).unwrap();
let end = Date::ymd(2024, 7, 1).unwrap();
assert_eq!(start.days_between(end), 91);
let f = fraction(start, end);
assert!((f - 91.0_f64 / 365.0).abs() < 1e-15);
assert!((f - 0.249_315_068_493_151).abs() < TOL);
}
#[test]
fn worked_2026_no_leap_in_2026() {
let start = Date::ymd(2026, 1, 1).unwrap();
let end = Date::ymd(2026, 4, 1).unwrap();
assert_eq!(start.days_between(end), 90);
let f = fraction(start, end);
assert!((f - 90.0_f64 / 365.0).abs() < 1e-15);
assert!((f - 0.246_575_342_465_753).abs() < TOL);
}
#[test]
fn straddle_leap_2024_to_2025() {
let start = Date::ymd(2023, 6, 1).unwrap();
let end = Date::ymd(2024, 6, 1).unwrap();
assert_eq!(start.days_between(end), 366);
let f = fraction(start, end);
assert!((f - 1.0).abs() < TOL);
}
#[test]
fn period_in_non_leap_year() {
let start = Date::ymd(2025, 1, 1).unwrap();
let end = Date::ymd(2026, 1, 1).unwrap();
assert_eq!(start.days_between(end), 365);
let f = fraction(start, end);
assert!((f - 1.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_negates_correctly() {
let start = Date::ymd(2024, 1, 1).unwrap();
let end = Date::ymd(2024, 4, 1).unwrap();
let forward = fraction(start, end);
let backward = fraction(end, start);
assert!((backward + forward).abs() < TOL);
assert!(backward < 0.0);
assert!((forward - 91.0_f64 / 366.0).abs() < TOL);
assert!((backward + 91.0_f64 / 366.0).abs() < TOL);
}
}