use crate::calendar::Calendar;
use crate::conventions::{AdjustRule, DayCount};
use crate::error::DayCountError;
use chrono::{Datelike, Days, NaiveDate};
pub fn is_business_day(date: &NaiveDate, calendar: &Calendar) -> bool {
if calendar.get_weekend().contains(&date.weekday()) {
return false;
}
!calendar.get_holidays().contains(date)
}
pub fn adjust(
date: &NaiveDate,
opt_calendar: Option<&Calendar>,
adjust_rule: Option<AdjustRule>,
) -> NaiveDate {
let calendar = match opt_calendar {
None => return *date,
Some(cal) => cal,
};
if is_business_day(date, calendar) {
return *date;
}
match adjust_rule {
None | Some(AdjustRule::Unadjusted) => *date,
Some(AdjustRule::Following) => add_adjust(date, calendar),
Some(AdjustRule::ModFollowing) => {
let adj = add_adjust(date, calendar);
if adj.month() != date.month() { sub_adjust(date, calendar) } else { adj }
}
Some(AdjustRule::Preceding) => sub_adjust(date, calendar),
Some(AdjustRule::ModPreceding) => {
let adj = sub_adjust(date, calendar);
if adj.month() != date.month() { add_adjust(date, calendar) } else { adj }
}
Some(AdjustRule::HalfMonthModFollowing) => {
let adj = add_adjust(date, calendar);
if adj.month() != date.month() || (date.day() <= 15 && adj.day() > 15) {
sub_adjust(date, calendar)
} else {
adj
}
}
Some(AdjustRule::Nearest) => {
let fwd = add_adjust(date, calendar);
let bwd = sub_adjust(date, calendar);
if (fwd - *date).num_days().abs() <= (bwd - *date).num_days().abs() {
fwd
} else {
bwd
}
}
}
}
fn add_adjust(date: &NaiveDate, calendar: &Calendar) -> NaiveDate {
let mut t = 1u64;
loop {
let candidate = date.checked_add_days(Days::new(t))
.unwrap_or_else(|| panic!("Date out of range while searching forward for business day"));
if is_business_day(&candidate, calendar) {
return candidate;
}
t += 1;
}
}
fn sub_adjust(date: &NaiveDate, calendar: &Calendar) -> NaiveDate {
let mut t = 1u64;
loop {
let candidate = date.checked_sub_days(Days::new(t))
.unwrap_or_else(|| panic!("Date out of range while searching backward for business day"));
if is_business_day(&candidate, calendar) {
return candidate;
}
t += 1;
}
}
pub fn bus_day_schedule(
start_date: &NaiveDate,
end_date: &NaiveDate,
calendar: &Calendar,
adjust_rule: Option<AdjustRule>,
) -> Vec<NaiveDate> {
let rule = adjust_rule.or(Some(AdjustRule::Following));
let new_start = adjust(start_date, Some(calendar), rule);
let new_end = adjust(end_date, Some(calendar), rule);
let mut schedule = vec![new_start];
let mut prev = new_start;
while prev < new_end {
let mut t = 1u64;
let mut next = adjust(
&prev.checked_add_days(Days::new(t)).unwrap(),
Some(calendar),
rule,
);
while next <= prev {
t += 1;
next = adjust(
&prev.checked_add_days(Days::new(t)).unwrap(),
Some(calendar),
rule,
);
}
schedule.push(next);
prev = next;
}
schedule
}
pub fn business_days_between(
start_date: &NaiveDate,
end_date: &NaiveDate,
calendar: &Calendar,
adjust_rule: Option<AdjustRule>,
) -> u64 {
let schedule = bus_day_schedule(start_date, end_date, calendar, adjust_rule);
schedule.len() as u64 - 1
}
pub fn day_count_fraction(
start_date: &NaiveDate,
end_date: &NaiveDate,
daycount: DayCount,
calendar: Option<&Calendar>,
adjust_rule: Option<AdjustRule>,
) -> Result<f64, DayCountError> {
let (start_adjusted, end_adjusted, some_adjust_rule, delta) = if calendar.is_none() {
(
*start_date,
*end_date,
adjust_rule,
(*end_date - *start_date).num_days().abs(),
)
} else {
let rule = if adjust_rule.is_none() {
Some(AdjustRule::Following)
} else {
adjust_rule
};
let s = adjust(start_date, calendar, rule);
let e = adjust(end_date, calendar, rule);
let d = (s - e).num_days().abs();
(s, e, rule, d)
};
let start_year: i32 = start_adjusted.year();
let start_month: i32 = start_adjusted.month() as i32;
let mut start_day: i32 = start_adjusted.day() as i32;
let end_year: i32 = end_adjusted.year();
let end_month: i32 = end_adjusted.month() as i32;
let mut end_day: i32 = end_adjusted.day() as i32;
match daycount {
DayCount::Act360 => Ok(delta as f64 / 360.0),
DayCount::Act365 => Ok(delta as f64 / 365.0),
DayCount::ActActISDA => {
if start_adjusted == end_adjusted {
return Ok(0.0);
}
if start_year == end_year && is_leap_year(start_year) {
return Ok(delta as f64 / 366.0);
}
if start_year == end_year {
return Ok(delta as f64 / 365.0);
}
if start_adjusted > end_adjusted {
return day_count_fraction(
&end_adjusted,
&start_adjusted,
DayCount::ActActISDA,
calendar,
some_adjust_rule,
);
}
let dcf = end_year as f64 - start_year as f64 - 1.0;
let base1 = if is_leap_year(start_year) { 366 } else { 365 };
let base2 = if is_leap_year(end_year) { 366 } else { 365 };
let dcf1 = (NaiveDate::from_ymd_opt(start_year + 1, 1, 1).unwrap()
- start_adjusted).num_days() as f64
/ base1 as f64;
let dcf2 = (end_adjusted
- NaiveDate::from_ymd_opt(end_year, 1, 1).unwrap()).num_days() as f64
/ base2 as f64;
Ok(dcf + dcf1 + dcf2)
}
DayCount::D30360Euro => {
if start_day == 31 { start_day = 30; }
if end_day == 31 { end_day = 30; }
let res = 360 * (end_year - start_year)
+ 30 * (end_month - start_month)
+ (end_day - start_day);
Ok(res as f64 / 360.0)
}
DayCount::D30365 => {
let res = 360.0 * (end_year - start_year) as f64
+ 30.0 * (end_month - start_month) as f64
+ (end_day - start_day) as f64;
Ok(res / 365.0)
}
DayCount::Bd252 => {
let cal = calendar.ok_or(DayCountError::MissingCalendar)?;
Ok(business_days_between(
&start_adjusted,
&end_adjusted,
cal,
some_adjust_rule,
) as f64 / 252.0)
}
}
}
pub fn checked_add_years(date: &NaiveDate, years_to_add: i32) -> Option<NaiveDate> {
NaiveDate::from_ymd_opt(
date.year() + years_to_add,
date.month(),
date.day(),
)
}
fn is_leap_year(year: i32) -> bool {
NaiveDate::from_ymd_opt(year, 2, 29).is_some()
}