use crate::shared::shared;
use crate::time::date::{Date, Month, SerialNumber};
use crate::time::daycounter::{DayCounter, DayCounterImpl};
use crate::types::{Integer, Time};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Convention {
Standard,
Canadian,
NoLeap,
}
pub struct Actual365Fixed;
impl Actual365Fixed {
pub fn new() -> DayCounter {
Self::with_convention(Convention::Standard)
}
pub fn with_convention(c: Convention) -> DayCounter {
match c {
Convention::Standard => DayCounter::from_impl(shared(StandardImpl)),
Convention::Canadian => DayCounter::from_impl(shared(CanadianImpl)),
Convention::NoLeap => DayCounter::from_impl(shared(NoLeapImpl)),
}
}
}
struct StandardImpl;
impl DayCounterImpl for StandardImpl {
fn name(&self) -> String {
"Actual/365 (Fixed)".to_string()
}
fn year_fraction(&self, d1: Date, d2: Date, _ref_start: Date, _ref_end: Date) -> Time {
Time::from(d2 - d1) / 365.0
}
}
struct CanadianImpl;
impl DayCounterImpl for CanadianImpl {
fn name(&self) -> String {
"Actual/365 (Fixed) Canadian Bond".to_string()
}
fn year_fraction(&self, d1: Date, d2: Date, ref_start: Date, ref_end: Date) -> Time {
if d1 == d2 {
return 0.0;
}
assert!(ref_start != Date::null(), "invalid refPeriodStart");
assert!(ref_end != Date::null(), "invalid refPeriodEnd");
assert!(
ref_end > ref_start,
"invalid reference period for Act/365 Canadian; end must be after start"
);
let dcs = Time::from(d2 - d1);
let dcc = Time::from(ref_end - ref_start);
let months = (12.0 * dcc / 365.0).round() as Integer;
assert!(
months != 0,
"invalid reference period for Act/365 Canadian; must be longer than a month"
);
let frequency = 12 / months;
assert!(
frequency != 0,
"invalid reference period for Act/365 Canadian; must not be longer than a year"
);
if dcs < Time::from(365 / frequency) {
return dcs / 365.0;
}
1.0 / Time::from(frequency) - (dcc - dcs) / 365.0
}
}
struct NoLeapImpl;
const MONTH_OFFSET: [SerialNumber; 12] = [
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, ];
impl DayCounterImpl for NoLeapImpl {
fn name(&self) -> String {
"Actual/365 (No Leap)".to_string()
}
fn day_count(&self, d1: Date, d2: Date) -> SerialNumber {
let serial = |d: Date| {
let mut s = d.day_of_month()
+ MONTH_OFFSET[(d.month().ordinal() - 1) as usize]
+ d.year() * 365;
if d.month() == Month::February && d.day_of_month() == 29 {
s -= 1;
}
s
};
serial(d2) - serial(d1)
}
fn year_fraction(&self, d1: Date, d2: Date, _ref_start: Date, _ref_end: Date) -> Time {
Time::from(self.day_count(d1, d2)) / 365.0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn d(day: SerialNumber, m: Month, y: SerialNumber) -> Date {
Date::new(day, m, y)
}
#[test]
fn standard_names_and_fraction() {
let dc = Actual365Fixed::new();
assert_eq!(dc.name(), "Actual/365 (Fixed)");
let start = d(1, Month::January, 2020);
let end = d(1, Month::July, 2020);
assert!((dc.year_fraction(start, end) - 182.0 / 365.0).abs() < 1e-12);
}
#[test]
fn canadian_name() {
assert_eq!(
Actual365Fixed::with_convention(Convention::Canadian).name(),
"Actual/365 (Fixed) Canadian Bond"
);
}
#[test]
fn canadian_semiannual_short_period() {
let dc = Actual365Fixed::with_convention(Convention::Canadian);
let d1 = d(10, Month::September, 2018);
let d2 = d(10, Month::October, 2018);
let rs = d(10, Month::September, 2018);
let re = d(10, Month::March, 2019); assert!((dc.year_fraction_ref(d1, d2, rs, re) - 30.0 / 365.0).abs() < 1e-12);
}
#[test]
#[should_panic(expected = "invalid refPeriodStart")]
fn canadian_without_reference_panics() {
let dc = Actual365Fixed::with_convention(Convention::Canadian);
dc.year_fraction(d(10, Month::September, 2018), d(10, Month::September, 2019));
}
#[test]
#[should_panic(expected = "must be longer than a month")]
fn canadian_short_reference_panics() {
let dc = Actual365Fixed::with_convention(Convention::Canadian);
dc.year_fraction_ref(
d(10, Month::September, 2018),
d(12, Month::September, 2018),
d(10, Month::September, 2018),
d(15, Month::September, 2018),
);
}
#[test]
#[should_panic(expected = "must not be longer than a year")]
fn canadian_long_reference_panics() {
let dc = Actual365Fixed::with_convention(Convention::Canadian);
dc.year_fraction_ref(
d(8, Month::January, 2025),
d(8, Month::January, 2027),
d(8, Month::January, 2025),
d(8, Month::January, 2027),
);
}
#[test]
#[should_panic(expected = "end must be after start")]
fn canadian_inverted_reference_panics() {
let dc = Actual365Fixed::with_convention(Convention::Canadian);
dc.year_fraction_ref(
d(10, Month::September, 2018),
d(10, Month::October, 2018),
d(10, Month::March, 2019),
d(10, Month::September, 2018),
);
}
#[test]
fn no_leap_skips_29_february() {
let dc = Actual365Fixed::with_convention(Convention::NoLeap);
assert_eq!(dc.name(), "Actual/365 (No Leap)");
let start = d(1, Month::January, 2020);
let end = d(1, Month::January, 2021);
assert_eq!(dc.day_count(start, end), 365);
assert!((dc.year_fraction(start, end) - 1.0).abs() < 1e-12);
}
#[test]
fn no_leap_spanning_leap_day() {
let dc = Actual365Fixed::with_convention(Convention::NoLeap);
let start = d(28, Month::February, 2020);
let end = d(1, Month::March, 2020);
assert_eq!(dc.day_count(start, end), 1);
}
}