use chrono::NaiveDate;
use findates::algebra::{day_count_fraction, day_count_fraction_act_act_icma};
use findates::calendar;
use findates::conventions::{AdjustRule, DayCount, Frequency};
use findates::DayCountError;
fn round_decimals(x: f64) -> f64 {
let multiplier = 100000.0;
(x * multiplier).round() / multiplier
}
struct DayCountSetup {
cal: calendar::Calendar,
}
impl DayCountSetup {
fn new() -> Self {
let mut basic_cal = calendar::basic_calendar();
let christmas_day = NaiveDate::from_ymd_opt(2023, 12, 25).unwrap();
let boxing_day = NaiveDate::from_ymd_opt(2023, 12, 26).unwrap();
basic_cal.add_holidays([christmas_day, boxing_day]);
Self { cal: basic_cal }
}
}
#[test]
fn dcf_act360_test() {
let setup = DayCountSetup::new();
let cal = setup.cal;
let start: NaiveDate = NaiveDate::from_ymd_opt(2023, 2, 15).unwrap();
let end: NaiveDate = NaiveDate::from_ymd_opt(2023, 9, 30).unwrap();
let expected: f64 = 0.6305556;
let res: f64 = day_count_fraction(&start, &end, DayCount::Act360, None, None).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
let start: NaiveDate = NaiveDate::from_ymd_opt(2023, 9, 30).unwrap(); let end: NaiveDate = NaiveDate::from_ymd_opt(2023, 12, 24).unwrap(); let expected: f64 = 0.2388889;
let res: f64 = day_count_fraction(
&start,
&end,
DayCount::Act360,
Some(&cal),
Some(AdjustRule::Following),
).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
}
#[test]
fn dcf_act365_test() {
let setup = DayCountSetup::new();
let cal = setup.cal;
let start: NaiveDate = NaiveDate::from_ymd_opt(2023, 2, 15).unwrap();
let end: NaiveDate = NaiveDate::from_ymd_opt(2023, 9, 30).unwrap();
let expected: f64 = 0.62191781;
let res: f64 = day_count_fraction(&start, &end, DayCount::Act365, None, None).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
let start: NaiveDate = NaiveDate::from_ymd_opt(2023, 9, 30).unwrap(); let end: NaiveDate = NaiveDate::from_ymd_opt(2023, 12, 24).unwrap(); let expected: f64 = 0.23561644;
let res: f64 = day_count_fraction(
&start,
&end,
DayCount::Act365,
Some(&cal),
Some(AdjustRule::Following),
).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
}
#[test]
fn dcf_actactisda_test() {
let setup = DayCountSetup::new();
let cal = setup.cal;
let start: NaiveDate = NaiveDate::from_ymd_opt(2024, 2, 15).unwrap();
let end: NaiveDate = NaiveDate::from_ymd_opt(2024, 5, 27).unwrap(); let expected: f64 = 0.27868852;
let res: f64 = day_count_fraction(&start, &end, DayCount::ActActISDA, Some(&cal), None).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
let start: NaiveDate = NaiveDate::from_ymd_opt(2023, 2, 15).unwrap();
let end: NaiveDate = NaiveDate::from_ymd_opt(2023, 5, 27).unwrap(); let expected: f64 = 0.28219178;
let res: f64 = day_count_fraction(&start, &end, DayCount::ActActISDA, Some(&cal), None).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
let start: NaiveDate = NaiveDate::from_ymd_opt(2023, 2, 15).unwrap();
let end: NaiveDate = NaiveDate::from_ymd_opt(2024, 5, 27).unwrap(); let expected: f64 = 1.27835167;
let res: f64 = day_count_fraction(&start, &end, DayCount::ActActISDA, Some(&cal), None).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
let start: NaiveDate = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(); let end: NaiveDate = NaiveDate::from_ymd_opt(2024, 5, 27).unwrap(); let expected: f64 = 4.23497268;
let res: f64 = day_count_fraction(&start, &end, DayCount::ActActISDA, Some(&cal), None).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
}
#[test]
fn dcf_d30360euro_test() {
let setup = DayCountSetup::new();
let cal = setup.cal;
let start: NaiveDate = NaiveDate::from_ymd_opt(2023, 1, 31).unwrap();
let end: NaiveDate = NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(); let expected: f64 = 1.04166667;
let res: f64 = day_count_fraction(&start, &end, DayCount::D30360Euro, Some(&cal), None).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
let start: NaiveDate = NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(); let end: NaiveDate = NaiveDate::from_ymd_opt(2024, 10, 31).unwrap();
let expected: f64 = 0.5805556;
let res: f64 = day_count_fraction(&start, &end, DayCount::D30360Euro, Some(&cal), None).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
let expected: f64 = 0.583333;
let res: f64 = day_count_fraction(&start, &end, DayCount::D30360Euro, None, None).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
}
#[test]
fn dcf_d30365_test() {
let setup = DayCountSetup::new();
let cal = setup.cal;
let start: NaiveDate = NaiveDate::from_ymd_opt(2023, 1, 24).unwrap();
let end: NaiveDate = NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(); let expected: f64 = 1.04383562;
let res: f64 = day_count_fraction(&start, &end, DayCount::D30365, Some(&cal), None).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
}
#[test]
fn dcf_bd252_test() {
let setup = DayCountSetup::new();
let cal = setup.cal;
let start: NaiveDate = NaiveDate::from_ymd_opt(2023, 1, 24).unwrap();
let end: NaiveDate = NaiveDate::from_ymd_opt(2024, 2, 15).unwrap();
let expected: f64 = 1.09126984;
let res: f64 = day_count_fraction(&start, &end, DayCount::Bd252, Some(&cal), None).unwrap();
assert_eq!(round_decimals(res), round_decimals(expected));
let start: NaiveDate = NaiveDate::from_ymd_opt(2023, 1, 24).unwrap();
let end: NaiveDate = NaiveDate::from_ymd_opt(2023, 12, 23).unwrap(); let end2: NaiveDate = NaiveDate::from_ymd_opt(2023, 12, 27).unwrap(); let res: f64 = day_count_fraction(&start, &end, DayCount::Bd252, Some(&cal), None).unwrap();
let res2: f64 = day_count_fraction(&start, &end2, DayCount::Bd252, Some(&cal), None).unwrap();
assert_eq!(round_decimals(res), round_decimals(res2));
let res: f64 = day_count_fraction(
&start,
&end,
DayCount::Bd252,
Some(&cal),
Some(AdjustRule::Preceding),
).unwrap();
let res2: f64 = day_count_fraction(
&start,
&end2,
DayCount::Bd252,
Some(&cal),
Some(AdjustRule::Preceding),
).unwrap();
assert_ne!(round_decimals(res), round_decimals(res2));
let expected: f64 = 0.94444444;
assert_eq!(round_decimals(res), round_decimals(expected));
}
#[test]
fn dcf_bd252_no_calendar_returns_err_test() {
let start: NaiveDate = NaiveDate::from_ymd_opt(2023, 1, 24).unwrap();
let end: NaiveDate = NaiveDate::from_ymd_opt(2024, 2, 15).unwrap();
assert_eq!(
day_count_fraction(&start, &end, DayCount::Bd252, None, None),
Err(DayCountError::MissingCalendar),
);
}
#[test]
fn dcf_bd252_with_calendar_returns_ok_test() {
let cal = calendar::basic_calendar();
let start = NaiveDate::from_ymd_opt(2024, 3, 18).unwrap();
let end = NaiveDate::from_ymd_opt(2024, 3, 22).unwrap();
assert!(day_count_fraction(&start, &end, DayCount::Bd252, Some(&cal), None).is_ok());
}
#[test]
fn dcf_act365fixed_leap_year_test() {
let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let end = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
let dcf = day_count_fraction(&start, &end, DayCount::Act365Fixed, None, None).unwrap();
assert!((dcf - 366.0 / 365.0).abs() < 1e-9);
}
#[test]
fn dcf_act365fixed_non_leap_year_test() {
let start = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
let end = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let dcf = day_count_fraction(&start, &end, DayCount::Act365Fixed, None, None).unwrap();
assert!((dcf - 1.0).abs() < 1e-9);
}
#[test]
fn dcf_thirty360us_start_on_31st_test() {
let start = NaiveDate::from_ymd_opt(2023, 1, 31).unwrap();
let end = NaiveDate::from_ymd_opt(2023, 4, 15).unwrap();
let dcf = day_count_fraction(&start, &end, DayCount::Thirty360US, None, None).unwrap();
assert!((dcf - 75.0 / 360.0).abs() < 1e-9);
}
#[test]
fn dcf_thirty360us_end_on_31st_start_on_30th_test() {
let start = NaiveDate::from_ymd_opt(2023, 1, 30).unwrap();
let end = NaiveDate::from_ymd_opt(2023, 3, 31).unwrap();
let dcf = day_count_fraction(&start, &end, DayCount::Thirty360US, None, None).unwrap();
assert!((dcf - 60.0 / 360.0).abs() < 1e-9);
}
#[test]
fn dcf_thirty360us_end_on_31st_start_before_30th_test() {
let start = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap();
let end = NaiveDate::from_ymd_opt(2023, 3, 31).unwrap();
let dcf = day_count_fraction(&start, &end, DayCount::Thirty360US, None, None).unwrap();
assert!((dcf - 76.0 / 360.0).abs() < 1e-9);
}
#[test]
fn dcf_thirty360us_start_on_eom_feb_nonleap_test() {
let start = NaiveDate::from_ymd_opt(2023, 2, 28).unwrap();
let end = NaiveDate::from_ymd_opt(2023, 6, 15).unwrap();
let dcf = day_count_fraction(&start, &end, DayCount::Thirty360US, None, None).unwrap();
assert!((dcf - 105.0 / 360.0).abs() < 1e-9);
}
#[test]
fn dcf_thirty360us_both_on_eom_feb_test() {
let start = NaiveDate::from_ymd_opt(2023, 2, 28).unwrap();
let end = NaiveDate::from_ymd_opt(2024, 2, 29).unwrap();
let dcf = day_count_fraction(&start, &end, DayCount::Thirty360US, None, None).unwrap();
assert!((dcf - 1.0).abs() < 1e-9);
}
#[test]
fn dcf_thirty360us_differs_from_d30360euro_for_february_test() {
let start = NaiveDate::from_ymd_opt(2023, 2, 28).unwrap();
let end = NaiveDate::from_ymd_opt(2023, 6, 15).unwrap();
let us = day_count_fraction(&start, &end, DayCount::Thirty360US, None, None).unwrap();
let eu = day_count_fraction(&start, &end, DayCount::D30360Euro, None, None).unwrap();
assert!((us - 105.0 / 360.0).abs() < 1e-9);
assert!((eu - 107.0 / 360.0).abs() < 1e-9);
assert_ne!(round_decimals(us), round_decimals(eu));
}
#[test]
fn dcf_non_bd252_conventions_return_ok_without_calendar_test() {
let start = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
let end = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
for dc in [
DayCount::Act360,
DayCount::Act365,
DayCount::Act365Fixed,
DayCount::ActActISDA,
DayCount::D30360Euro,
DayCount::Thirty360US,
DayCount::D30365,
] {
assert!(
day_count_fraction(&start, &end, dc, None, None).is_ok(),
"{dc} should return Ok without a calendar",
);
}
}
#[test]
fn dcf_actacticma_semiannual_partial_period_test() {
let date1 = NaiveDate::from_ymd_opt(2003, 11, 1).unwrap();
let date2 = NaiveDate::from_ymd_opt(2003, 12, 31).unwrap();
let period_start = NaiveDate::from_ymd_opt(2003, 11, 1).unwrap();
let period_end = NaiveDate::from_ymd_opt(2004, 5, 1).unwrap();
let dcf = day_count_fraction_act_act_icma(
&date1,
&date2,
&period_start,
&period_end,
Frequency::Semiannual,
).unwrap();
assert!((dcf - 60.0 / (2.0 * 182.0)).abs() < 1e-9);
}
#[test]
fn dcf_actacticma_semiannual_full_period_test() {
let period_start = NaiveDate::from_ymd_opt(2003, 11, 1).unwrap();
let period_end = NaiveDate::from_ymd_opt(2004, 5, 1).unwrap();
let dcf = day_count_fraction_act_act_icma(
&period_start,
&period_end,
&period_start,
&period_end,
Frequency::Semiannual,
).unwrap();
assert!((dcf - 0.5).abs() < 1e-9);
}
#[test]
fn dcf_actacticma_annual_full_period_test() {
let period_start = NaiveDate::from_ymd_opt(1999, 7, 1).unwrap();
let period_end = NaiveDate::from_ymd_opt(2000, 7, 1).unwrap();
let dcf = day_count_fraction_act_act_icma(
&period_start,
&period_end,
&period_start,
&period_end,
Frequency::Annual,
).unwrap();
assert!((dcf - 1.0).abs() < 1e-9);
}
#[test]
fn dcf_actacticma_quarterly_full_period_test() {
let period_start = NaiveDate::from_ymd_opt(1999, 11, 30).unwrap();
let period_end = NaiveDate::from_ymd_opt(2000, 2, 29).unwrap();
let dcf = day_count_fraction_act_act_icma(
&period_start,
&period_end,
&period_start,
&period_end,
Frequency::Quarterly,
).unwrap();
assert!((dcf - 0.25).abs() < 1e-9);
}
#[test]
fn dcf_actacticma_via_day_count_fraction_returns_err_test() {
let start = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
let end = NaiveDate::from_ymd_opt(2023, 7, 1).unwrap();
assert_eq!(
day_count_fraction(&start, &end, DayCount::ActActICMA, None, None),
Err(DayCountError::RequiresPeriodContext)
);
}