use chrono::Datelike;
use chrono::NaiveDate;
use crate::traits::FloatExt;
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DayCountConvention {
Actual360,
#[default]
Actual365Fixed,
Thirty360,
Thirty360European,
ActualActualISDA,
}
impl std::fmt::Display for DayCountConvention {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Actual360 => write!(f, "ACT/360"),
Self::Actual365Fixed => write!(f, "ACT/365F"),
Self::Thirty360 => write!(f, "30/360"),
Self::Thirty360European => write!(f, "30E/360"),
Self::ActualActualISDA => write!(f, "ACT/ACT ISDA"),
}
}
}
impl DayCountConvention {
pub fn year_fraction<T: FloatExt>(&self, d1: NaiveDate, d2: NaiveDate) -> T {
match self {
Self::Actual360 => {
let days = (d2 - d1).num_days() as f64;
T::from_f64_fast(days / 360.0)
}
Self::Actual365Fixed => {
let days = (d2 - d1).num_days() as f64;
T::from_f64_fast(days / 365.0)
}
Self::Thirty360 => {
let num = self.day_count(d1, d2) as f64;
T::from_f64_fast(num / 360.0)
}
Self::Thirty360European => {
let num = self.day_count(d1, d2) as f64;
T::from_f64_fast(num / 360.0)
}
Self::ActualActualISDA => T::from_f64_fast(actual_actual_isda(d1, d2)),
}
}
pub fn day_count(&self, d1: NaiveDate, d2: NaiveDate) -> i64 {
match self {
Self::Actual360 | Self::Actual365Fixed | Self::ActualActualISDA => (d2 - d1).num_days(),
Self::Thirty360 => thirty360_usa(d1, d2),
Self::Thirty360European => thirty360_european(d1, d2),
}
}
}
fn thirty360_usa(d1: NaiveDate, d2: NaiveDate) -> i64 {
let (y1, m1) = (d1.year() as i64, d1.month() as i64);
let (y2, m2) = (d2.year() as i64, d2.month() as i64);
let mut dd1 = d1.day() as i64;
let mut dd2 = d2.day() as i64;
if dd1 == 31 {
dd1 = 30;
}
if dd2 == 31 && dd1 == 30 {
dd2 = 30;
}
360 * (y2 - y1) + 30 * (m2 - m1) + (dd2 - dd1)
}
fn thirty360_european(d1: NaiveDate, d2: NaiveDate) -> i64 {
let (y1, m1) = (d1.year() as i64, d1.month() as i64);
let (y2, m2) = (d2.year() as i64, d2.month() as i64);
let dd1 = (d1.day() as i64).min(30);
let dd2 = (d2.day() as i64).min(30);
360 * (y2 - y1) + 30 * (m2 - m1) + (dd2 - dd1)
}
fn actual_actual_isda(d1: NaiveDate, d2: NaiveDate) -> f64 {
if d1 == d2 {
return 0.0;
}
let y1 = d1.year();
let y2 = d2.year();
if y1 == y2 {
let days = (d2 - d1).num_days() as f64;
let denom = if is_leap_year(y1) { 366.0 } else { 365.0 };
return days / denom;
}
let end_of_y1 = NaiveDate::from_ymd_opt(y1 + 1, 1, 1).unwrap();
let days_first = (end_of_y1 - d1).num_days() as f64;
let denom_first = if is_leap_year(y1) { 366.0 } else { 365.0 };
let start_of_y2 = NaiveDate::from_ymd_opt(y2, 1, 1).unwrap();
let days_last = (d2 - start_of_y2).num_days() as f64;
let denom_last = if is_leap_year(y2) { 366.0 } else { 365.0 };
let full_years = (y2 - y1 - 1) as f64;
days_first / denom_first + full_years + days_last / denom_last
}
pub(crate) fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
pub(crate) fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => unreachable!(),
}
}
#[cfg(test)]
mod tests {
use chrono::NaiveDate;
use super::*;
#[test]
fn act365_full_year() {
let d1 = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let d2 = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
let yf: f64 = DayCountConvention::Actual365Fixed.year_fraction(d1, d2);
assert!((yf - 366.0 / 365.0).abs() < 1e-12);
}
#[test]
fn act360_full_year() {
let d1 = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
let d2 = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let yf: f64 = DayCountConvention::Actual360.year_fraction(d1, d2);
assert!((yf - 365.0 / 360.0).abs() < 1e-12);
}
#[test]
fn thirty360_full_year() {
let d1 = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap();
let d2 = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
let yf: f64 = DayCountConvention::Thirty360.year_fraction(d1, d2);
assert!((yf - 1.0).abs() < 1e-12);
}
#[test]
fn leap_year_detection() {
assert!(is_leap_year(2024));
assert!(!is_leap_year(2023));
assert!(!is_leap_year(1900));
assert!(is_leap_year(2000));
}
#[test]
fn days_in_february_leap() {
assert_eq!(days_in_month(2024, 2), 29);
assert_eq!(days_in_month(2023, 2), 28);
}
}