use chrono::{Datelike, NaiveDate};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use thiserror::Error;
#[allow(clippy::if_same_then_else)]
pub fn is_leap_year(year: i32) -> bool {
if year % 4 > 0 {
false
} else if year % 100 > 0 {
true
} else if year % 400 == 0 {
true
} else {
false
}
}
pub fn is_end_of_month(day: u32, month: u32, year: i32) -> bool {
if [1, 3, 5, 7, 8, 10, 12].contains(&month) {
day == 31
} else if [4, 6, 9, 11].contains(&month) {
day == 30
} else if is_leap_year(year) {
day == 29
} else {
day == 28
}
}
#[derive(Hash, Clone, Copy, Debug, Serialize, Deserialize)]
pub enum DayCountConvention {
US30360,
ActAct,
Act360,
Act365,
EU30360,
}
impl DayCountConvention {
pub fn from_int(day_count_convention: u8) -> Result<Self, DayCountConventionError> {
match day_count_convention {
0 => Ok(DayCountConvention::US30360),
1 => Ok(DayCountConvention::ActAct),
2 => Ok(DayCountConvention::Act360),
3 => Ok(DayCountConvention::Act365),
4 => Ok(DayCountConvention::EU30360),
other => Err(DayCountConventionError::InvalidValue {
val: other.to_string(),
}),
}
}
pub fn from_str(day_count_convention: &str) -> Result<Self, DayCountConventionError> {
<Self as FromStr>::from_str(day_count_convention)
}
pub fn yearfrac(&self, mut start: NaiveDate, mut end: NaiveDate) -> f64 {
if start == end {
return 0.0; } else if start > end {
(start, end) = (end, start)
}
let numerator = self.diff_dts(start, end);
let denom = self.basis(start, end);
numerator / denom
}
pub fn yearfrac_signed(&self, start: NaiveDate, end: NaiveDate) -> f64 {
if start > end {
-self.yearfrac(start, end)
} else {
self.yearfrac(start, end)
}
}
fn basis(&self, start: NaiveDate, end: NaiveDate) -> f64 {
match self {
DayCountConvention::US30360
| DayCountConvention::Act360
| DayCountConvention::EU30360 => 360.0,
DayCountConvention::Act365 => 365.0,
DayCountConvention::ActAct => {
let (start_day, start_month, start_year) =
(start.day(), start.month(), start.year());
let (end_day, end_month, end_year) = (end.day(), end.month(), end.year());
if start_year == end_year {
if is_leap_year(start_year) {
366.0
} else {
365.0
}
} else if (end_year - 1 == start_year)
& ((start_month > end_month)
| ((start_month == end_month) & (start_day > end_day)))
{
if is_leap_year(start_year) {
if (start_month < 2) | ((start_month == 2) & (start_day <= 29)) {
366.0
} else {
365.0
}
} else if is_leap_year(end_year) {
if (end_month > 2) | ((end_month == 2) & (end_day == 29)) {
366.0
} else {
365.0
}
} else {
365.0
}
} else {
let mut tmp = 0.0;
for i_y in start_year..end_year + 1 {
if is_leap_year(i_y) {
tmp += 366.0
} else {
tmp += 365.0
}
}
tmp / (end_year as f64 - start_year as f64 + 1.0)
}
}
}
}
fn diff_dts(&self, start: NaiveDate, end: NaiveDate) -> f64 {
match self {
DayCountConvention::ActAct
| DayCountConvention::Act360
| DayCountConvention::Act365 => (end - start).num_days() as f64,
DayCountConvention::US30360 => self.nasd360(start, end, 0, true),
DayCountConvention::EU30360 => self.euro360(start, end),
}
}
fn euro360(&self, start: NaiveDate, end: NaiveDate) -> f64 {
let (mut start_day, start_month, start_year) = (start.day(), start.month(), start.year());
let (mut end_day, end_month, end_year) = (end.day(), end.month(), end.year());
if start_day == 31 {
start_day = 30;
};
if end_day == 31 {
end_day = 30;
};
self.days360(
start_day,
start_month,
start_year,
end_day,
end_month,
end_year,
)
}
fn nasd360(&self, start: NaiveDate, end: NaiveDate, method: u8, use_eom: bool) -> f64 {
let (mut start_day, start_month, start_year) = (start.day(), start.month(), start.year());
let (mut end_day, end_month, end_year) = (end.day(), end.month(), end.year());
if ((end_month == 2) & is_end_of_month(end_day, end_month, end_year))
& (((start_month == 2) & is_end_of_month(start_day, start_month, start_year))
| (method == 3))
{
end_day = 30;
};
if (end_day == 31) & ((start_day >= 30) | (method == 3)) {
end_day = 30;
};
if start_day == 31 {
start_day = 30;
}
if use_eom & (start_month == 2) & is_end_of_month(start_day, start_month, start_year) {
start_day = 30;
}
self.days360(
start_day,
start_month,
start_year,
end_day,
end_month,
end_year,
)
}
fn days360(
&self,
start_day: u32,
start_month: u32,
start_year: i32,
end_day: u32,
end_month: u32,
end_year: i32,
) -> f64 {
((end_year - start_year) * 360
+ (end_month as i32 - start_month as i32) * 30
+ (end_day as i32 - start_day as i32))
.into()
}
}
impl FromStr for DayCountConvention {
type Err = DayCountConventionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"nasd30/360" => Ok(DayCountConvention::US30360),
"act/act" => Ok(DayCountConvention::ActAct),
"act360" => Ok(DayCountConvention::Act360),
"act365" => Ok(DayCountConvention::Act365),
"eur30/360" => Ok(DayCountConvention::EU30360),
other => Err(DayCountConventionError::InvalidValue {
val: other.to_owned(),
}),
}
}
}
#[derive(Debug, Error)]
pub enum DayCountConventionError {
#[error("Yearfrac: Invalid Value: {}. Has to be one of: nasd30/360, act/act, act360, act365, eur30/360 (from_str)
or in the range 0-4 (from_int).", val)]
InvalidValue { val: String },
}
#[cfg(test)]
mod tests {}