use crate::shared::shared;
use crate::time::date::{Date, Day, Month, SerialNumber, Year};
use crate::time::daycounter::{DayCounter, DayCounterImpl};
use crate::types::Time;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(clippy::upper_case_acronyms)]
pub enum Convention {
USA,
BondBasis,
European,
EurobondBasis,
Italian,
German,
ISMA,
ISDA,
NASD,
}
pub struct Thirty360;
impl Thirty360 {
pub fn with_convention(c: Convention) -> DayCounter {
Self::with_termination(c, Date::null())
}
pub fn with_termination(c: Convention, termination: Date) -> DayCounter {
let rule = match c {
Convention::USA => Rule::Us,
Convention::BondBasis | Convention::ISMA => Rule::BondBasis,
Convention::European | Convention::EurobondBasis => Rule::Eurobond,
Convention::Italian => Rule::Italian,
Convention::German | Convention::ISDA => Rule::Isda,
Convention::NASD => Rule::Nasd,
};
DayCounter::from_impl(shared(Impl { rule, termination }))
}
}
#[derive(Clone, Copy)]
enum Rule {
Us,
BondBasis,
Eurobond,
Italian,
Isda,
Nasd,
}
struct Impl {
rule: Rule,
termination: Date,
}
fn is_last_of_february(d: Day, m: Month, y: Year) -> bool {
m == Month::February && d == 28 + SerialNumber::from(Date::is_leap(y))
}
fn thirty_360(yy1: Year, yy2: Year, mm1: Day, mm2: Day, dd1: Day, dd2: Day) -> SerialNumber {
360 * (yy2 - yy1) + 30 * (mm2 - mm1) + (dd2 - dd1)
}
impl DayCounterImpl for Impl {
fn name(&self) -> String {
match self.rule {
Rule::Us => "30/360 (US)",
Rule::BondBasis => "30/360 (Bond Basis)",
Rule::Eurobond => "30E/360 (Eurobond Basis)",
Rule::Italian => "30/360 (Italian)",
Rule::Isda => "30E/360 (ISDA)",
Rule::Nasd => "30/360 (NASD)",
}
.to_string()
}
fn day_count(&self, d1: Date, d2: Date) -> SerialNumber {
let (mut dd1, mut dd2) = (d1.day_of_month(), d2.day_of_month());
let (mm1, mm2) = (d1.month(), d2.month());
let (yy1, yy2) = (d1.year(), d2.year());
let (om1, mut om2) = (mm1.ordinal(), mm2.ordinal());
match self.rule {
Rule::Us => {
if is_last_of_february(dd1, mm1, yy1) {
if is_last_of_february(dd2, mm2, yy2) {
dd2 = 30;
}
dd1 = 30;
}
if dd2 == 31 && dd1 >= 30 {
dd2 = 30;
}
if dd1 == 31 {
dd1 = 30;
}
}
Rule::BondBasis => {
if dd1 == 31 {
dd1 = 30;
}
if dd2 == 31 && dd1 == 30 {
dd2 = 30;
}
}
Rule::Eurobond => {
if dd1 == 31 {
dd1 = 30;
}
if dd2 == 31 {
dd2 = 30;
}
}
Rule::Italian => {
if dd1 == 31 {
dd1 = 30;
}
if dd2 == 31 {
dd2 = 30;
}
if mm1 == Month::February && dd1 > 27 {
dd1 = 30;
}
if mm2 == Month::February && dd2 > 27 {
dd2 = 30;
}
}
Rule::Isda => {
if dd1 == 31 {
dd1 = 30;
}
if dd2 == 31 {
dd2 = 30;
}
if is_last_of_february(dd1, mm1, yy1) {
dd1 = 30;
}
if d2 != self.termination && is_last_of_february(dd2, mm2, yy2) {
dd2 = 30;
}
}
Rule::Nasd => {
if dd1 == 31 {
dd1 = 30;
}
if dd2 == 31 && dd1 >= 30 {
dd2 = 30;
}
if dd2 == 31 && dd1 < 30 {
dd2 = 1;
om2 += 1;
}
}
}
thirty_360(yy1, yy2, om1, om2, dd1, dd2)
}
fn year_fraction(&self, d1: Date, d2: Date, _ref_start: Date, _ref_end: Date) -> Time {
Time::from(self.day_count(d1, d2)) / 360.0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn d(day: SerialNumber, m: Month, y: SerialNumber) -> Date {
Date::new(day, m, y)
}
#[test]
fn names_match_quantlib() {
assert_eq!(
Thirty360::with_convention(Convention::USA).name(),
"30/360 (US)"
);
assert_eq!(
Thirty360::with_convention(Convention::BondBasis).name(),
"30/360 (Bond Basis)"
);
assert_eq!(
Thirty360::with_convention(Convention::EurobondBasis).name(),
"30E/360 (Eurobond Basis)"
);
assert_eq!(
Thirty360::with_convention(Convention::Italian).name(),
"30/360 (Italian)"
);
assert_eq!(
Thirty360::with_convention(Convention::ISDA).name(),
"30E/360 (ISDA)"
);
assert_eq!(
Thirty360::with_convention(Convention::NASD).name(),
"30/360 (NASD)"
);
assert_eq!(
Thirty360::with_convention(Convention::ISMA).name(),
Thirty360::with_convention(Convention::BondBasis).name()
);
assert_eq!(
Thirty360::with_convention(Convention::German).name(),
Thirty360::with_convention(Convention::ISDA).name()
);
assert_eq!(
Thirty360::with_convention(Convention::European).name(),
Thirty360::with_convention(Convention::EurobondBasis).name()
);
}
#[test]
fn usa_known_good_values() {
let dc = Thirty360::with_convention(Convention::USA);
let cases: &[(Date, Date, SerialNumber)] = &[
(
d(20, Month::August, 2006),
d(20, Month::February, 2007),
180,
),
(
d(31, Month::August, 2006),
d(28, Month::February, 2007),
178,
),
(
d(31, Month::August, 2007),
d(29, Month::February, 2008),
179,
),
(
d(28, Month::February, 2008),
d(31, Month::August, 2008),
183,
),
(
d(28, Month::February, 2008),
d(30, Month::August, 2008),
182,
),
(
d(29, Month::February, 2008),
d(28, Month::February, 2009),
360,
),
(d(28, Month::February, 2008), d(31, Month::March, 2008), 33),
(d(28, Month::February, 2006), d(3, Month::March, 2006), 3),
(
d(30, Month::September, 2006),
d(31, Month::October, 2006),
30,
),
];
for &(start, end, expected) in cases {
assert_eq!(dc.day_count(start, end), expected, "{start} -> {end}");
}
let (s, e) = (d(20, Month::August, 2006), d(20, Month::February, 2007));
assert!((dc.year_fraction(s, e) - 180.0 / 360.0).abs() < 1e-12);
}
#[test]
fn eurobond_end_of_february_is_not_special() {
let dc = Thirty360::with_convention(Convention::EurobondBasis);
assert_eq!(
dc.day_count(d(28, Month::February, 2006), d(31, Month::August, 2006)),
182
);
assert_eq!(
dc.day_count(d(31, Month::August, 2007), d(29, Month::February, 2008)),
179
);
}
#[test]
fn isda_treats_last_of_february_as_thirty_except_termination() {
let dc = Thirty360::with_termination(Convention::ISDA, d(20, Month::August, 2009));
assert_eq!(
dc.day_count(d(28, Month::February, 2006), d(31, Month::August, 2006)),
180
);
assert_eq!(
dc.day_count(d(31, Month::August, 2007), d(29, Month::February, 2008)),
180
);
}
#[test]
fn nasd_carries_into_next_month() {
let dc = Thirty360::with_convention(Convention::NASD);
assert_eq!(
dc.day_count(d(15, Month::January, 2006), d(31, Month::January, 2006)),
16
);
}
}