rustyqlib/core/
daycount.rs1use chrono::{Datelike, NaiveDate};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
10pub enum DayCountConvention {
11 #[default]
13 #[serde(alias = "Act/365", alias = "ACT/365", alias = "act365", alias = "A365")]
14 Act365,
15 #[serde(alias = "Act/360", alias = "ACT/360", alias = "act360", alias = "A360")]
17 Act360,
18 #[serde(alias = "30/360", alias = "thirty360")]
20 Thirty360,
21}
22
23impl DayCountConvention {
24 pub fn year_fraction(&self, start: NaiveDate, end: NaiveDate) -> f64 {
27 match self {
28 DayCountConvention::Act365 => (end - start).num_days() as f64 / 365.0,
29 DayCountConvention::Act360 => (end - start).num_days() as f64 / 360.0,
30 DayCountConvention::Thirty360 => {
31 let (y1, m1, mut d1) = (start.year(), start.month() as i64, start.day() as i64);
32 let (y2, m2, mut d2) = (end.year(), end.month() as i64, end.day() as i64);
33 if d1 == 31 {
34 d1 = 30;
35 }
36 if d2 == 31 && d1 == 30 {
37 d2 = 30;
38 }
39 (360 * (y2 - y1) as i64 + 30 * (m2 - m1) + (d2 - d1)) as f64 / 360.0
40 }
41 }
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 fn d(y: i32, m: u32, day: u32) -> NaiveDate {
50 NaiveDate::from_ymd_opt(y, m, day).unwrap()
51 }
52
53 #[test]
54 fn act365_one_year() {
55 let yf = DayCountConvention::Act365.year_fraction(d(2026, 1, 1), d(2027, 1, 1));
56 assert!((yf - 1.0).abs() < 1e-12);
57 }
58
59 #[test]
60 fn act360_ninety_days() {
61 let yf = DayCountConvention::Act360.year_fraction(d(2026, 1, 1), d(2026, 4, 1));
62 assert!((yf - 90.0 / 360.0).abs() < 1e-12);
63 }
64
65 #[test]
66 fn thirty360_half_year_month_ends() {
67 let yf = DayCountConvention::Thirty360.year_fraction(d(2026, 1, 31), d(2026, 7, 31));
69 assert!((yf - 0.5).abs() < 1e-12);
70 }
71}