use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DayCount {
Act360,
Act365Fixed,
Thirty360,
ActAct,
}
impl DayCount {
pub fn year_fraction(&self, start: NaiveDate, end: NaiveDate) -> f64 {
if end <= start {
return 0.0;
}
match self {
DayCount::Act360 => {
let days = (end - start).num_days() as f64;
days / 360.0
}
DayCount::Act365Fixed => {
let days = (end - start).num_days() as f64;
days / 365.0
}
DayCount::Thirty360 => thirty360_us(start, end),
DayCount::ActAct => act_act_isda(start, end),
}
}
}
fn thirty360_us(start: NaiveDate, end: NaiveDate) -> f64 {
use chrono::Datelike;
let y1 = start.year();
let m1 = start.month() as i32;
let mut d1 = start.day() as i32;
let y2 = end.year();
let m2 = end.month() as i32;
let mut d2 = end.day() as i32;
if d1 == 31 {
d1 = 30;
}
if d2 == 31 && d1 >= 30 {
d2 = 30;
}
let numerator = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1);
numerator as f64 / 360.0
}
fn act_act_isda(start: NaiveDate, end: NaiveDate) -> f64 {
use chrono::Datelike;
if start.year() == end.year() {
let days = (end - start).num_days() as f64;
let days_in_year = if is_leap_year(start.year()) {
366.0
} else {
365.0
};
return days / days_in_year;
}
let mut total = 0.0_f64;
let start_year_end = NaiveDate::from_ymd_opt(start.year() + 1, 1, 1).expect("year+1 is valid");
let days_in_start_year = if is_leap_year(start.year()) {
366.0
} else {
365.0
};
total += (start_year_end - start).num_days() as f64 / days_in_start_year;
let mut y = start.year() + 1;
while y < end.year() {
let days_in_y = if is_leap_year(y) { 366.0 } else { 365.0 };
let _ = days_in_y; total += 1.0; y += 1;
}
let end_year_start = NaiveDate::from_ymd_opt(end.year(), 1, 1).expect("end year is valid");
let days_in_end_year = if is_leap_year(end.year()) {
366.0
} else {
365.0
};
total += (end - end_year_start).num_days() as f64 / days_in_end_year;
total
}
fn is_leap_year(y: i32) -> bool {
(y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, day).unwrap()
}
#[test]
fn act360_non_leap_half_year() {
let yf = DayCount::Act360.year_fraction(d(2019, 1, 1), d(2019, 7, 1));
assert!((yf - 181.0 / 360.0).abs() < 1e-10, "yf={yf}");
}
#[test]
fn act360_leap_year() {
let yf = DayCount::Act360.year_fraction(d(2020, 1, 1), d(2020, 7, 1));
assert!((yf - 182.0 / 360.0).abs() < 1e-10, "yf={yf}");
}
#[test]
fn act360_zero_when_same_date() {
let yf = DayCount::Act360.year_fraction(d(2020, 6, 1), d(2020, 6, 1));
assert_eq!(yf, 0.0);
}
#[test]
fn act365_full_year_non_leap() {
let yf = DayCount::Act365Fixed.year_fraction(d(2019, 1, 1), d(2020, 1, 1));
assert!((yf - 1.0).abs() < 1e-10, "yf={yf}");
}
#[test]
fn act365_leap_year_is_not_1() {
let yf = DayCount::Act365Fixed.year_fraction(d(2020, 1, 1), d(2021, 1, 1));
assert!((yf - 366.0 / 365.0).abs() < 1e-10, "yf={yf}");
}
#[test]
fn act365_182_days() {
let yf = DayCount::Act365Fixed.year_fraction(d(2020, 1, 1), d(2020, 7, 1));
assert!((yf - 182.0 / 365.0).abs() < 1e-10, "yf={yf}");
}
#[test]
fn thirty360_half_year_exact() {
let yf = DayCount::Thirty360.year_fraction(d(2007, 1, 15), d(2007, 7, 15));
assert!((yf - 0.5).abs() < 1e-10, "yf={yf}");
}
#[test]
fn thirty360_both_31() {
let yf = DayCount::Thirty360.year_fraction(d(2020, 1, 31), d(2020, 7, 31));
assert!((yf - 0.5).abs() < 1e-10, "yf={yf}");
}
#[test]
fn thirty360_d2_31_d1_28() {
let yf = DayCount::Thirty360.year_fraction(d(2020, 2, 28), d(2020, 3, 31));
let expected = 33.0 / 360.0;
assert!(
(yf - expected).abs() < 1e-10,
"yf={yf}, expected={expected}"
);
}
#[test]
fn thirty360_full_year() {
let yf = DayCount::Thirty360.year_fraction(d(2020, 1, 1), d(2021, 1, 1));
assert!((yf - 1.0).abs() < 1e-10, "yf={yf}");
}
#[test]
fn actact_within_non_leap_year() {
let yf = DayCount::ActAct.year_fraction(d(2019, 3, 1), d(2019, 9, 1));
assert!((yf - 184.0 / 365.0).abs() < 1e-10, "yf={yf}");
}
#[test]
fn actact_within_leap_year() {
let yf = DayCount::ActAct.year_fraction(d(2020, 1, 1), d(2020, 7, 1));
assert!((yf - 182.0 / 366.0).abs() < 1e-10, "yf={yf}");
}
#[test]
fn actact_spanning_year_boundary() {
let start = d(2019, 7, 1);
let end = d(2020, 7, 1);
let yf = DayCount::ActAct.year_fraction(start, end);
let expected = 184.0 / 365.0 + 182.0 / 366.0;
assert!(
(yf - expected).abs() < 1e-10,
"yf={yf}, expected={expected}"
);
}
#[test]
fn actact_full_non_leap_year() {
let yf = DayCount::ActAct.year_fraction(d(2019, 1, 1), d(2020, 1, 1));
assert!((yf - 1.0).abs() < 1e-10, "yf={yf}");
}
#[test]
fn actact_full_leap_year() {
let yf = DayCount::ActAct.year_fraction(d(2020, 1, 1), d(2021, 1, 1));
assert!((yf - 1.0).abs() < 1e-10, "yf={yf}");
}
#[test]
fn zero_year_fraction() {
let dt = d(2020, 6, 15);
for conv in &[
DayCount::Act360,
DayCount::Act365Fixed,
DayCount::Thirty360,
DayCount::ActAct,
] {
assert_eq!(conv.year_fraction(dt, dt), 0.0, "conv={conv:?}");
}
}
#[test]
fn leap_year_detection() {
assert!(is_leap_year(2020));
assert!(!is_leap_year(2019));
assert!(!is_leap_year(1900)); assert!(is_leap_year(2000)); }
}