use chrono::{Duration, Months, NaiveDate, TimeDelta};
use serde::{Deserialize, Serialize};
use std::ops::{Add, Mul, Sub};
use crate::error::{Error, Result};
use crate::time::calendars::Calendar;
use crate::utils::const_unwrap;
pub const ONE_DAY: TimeDelta = const_unwrap!(Duration::try_days(1));
pub const TWO_DAYS: TimeDelta = const_unwrap!(Duration::try_days(2));
#[derive(Deserialize, Serialize, PartialEq, Clone, Copy, Debug)]
pub enum Period {
ON,
TN,
SPOT,
SN,
Days(i64),
Weeks(i64),
Months(u32),
Years(u32),
}
impl Period {
pub fn settlement_date(
&self,
valuation_date: NaiveDate,
calendar: &dyn Calendar,
) -> Result<NaiveDate> {
self.settlement_date_with_lag(valuation_date, calendar, 2)
}
pub fn near_date(
&self,
valuation_date: NaiveDate,
calendar: &dyn Calendar,
) -> Result<Option<NaiveDate>> {
self.near_date_with_lag(valuation_date, calendar, 2)
}
pub(crate) fn settlement_date_with_lag(
&self,
valuation_date: NaiveDate,
calendar: &dyn Calendar,
spot_lag: i64,
) -> Result<NaiveDate> {
let spot_offset = Duration::try_days(spot_lag).ok_or_else(|| {
Error::PeriodOutOfBounds(format!("{spot_lag} spot lag is out of bounds"))
})?;
let target_date = match self {
Period::ON => valuation_date,
Period::TN => valuation_date + ONE_DAY,
_ => valuation_date + spot_offset,
};
let mut settlement_date = (target_date + *self)?;
if settlement_date >= calendar.end_of_month(settlement_date) {
settlement_date = calendar.end_of_month(settlement_date)
}
while !calendar.is_business_day(settlement_date) {
settlement_date += ONE_DAY;
}
Ok(settlement_date)
}
pub(crate) fn near_date_with_lag(
&self,
valuation_date: NaiveDate,
calendar: &dyn Calendar,
spot_lag: i64,
) -> Result<Option<NaiveDate>> {
let spot_offset = Duration::try_days(spot_lag).ok_or_else(|| {
Error::PeriodOutOfBounds(format!("{spot_lag} spot lag is out of bounds"))
})?;
let raw = match self {
Period::ON => Some(valuation_date),
Period::TN => Some(valuation_date + ONE_DAY),
Period::SN => Some(valuation_date + spot_offset),
_ => None,
};
match raw {
None => Ok(None),
Some(mut d) => {
while !calendar.is_business_day(d) {
d += ONE_DAY;
}
Ok(Some(d))
}
}
}
}
impl Add<Period> for NaiveDate {
type Output = Result<NaiveDate>;
fn add(self, rhs: Period) -> Self::Output {
let date = match rhs {
Period::ON => self + ONE_DAY,
Period::TN => self + ONE_DAY,
Period::SPOT => self,
Period::SN => self + ONE_DAY,
Period::Days(num) => {
self + Duration::try_days(num).ok_or_else(|| {
Error::PeriodOutOfBounds(format!("{num} days is out of bounds"))
})?
}
Period::Weeks(num) => {
self + Duration::try_days(num * 7).ok_or_else(|| {
Error::PeriodOutOfBounds(format!("{num} weeks is out of bounds"))
})?
}
Period::Months(num) => self + Months::new(num),
Period::Years(num) => self + Months::new(num * 12),
};
Ok(date)
}
}
impl Sub<Period> for NaiveDate {
type Output = Result<NaiveDate>;
fn sub(self, rhs: Period) -> Self::Output {
let date = match rhs {
Period::ON => self - ONE_DAY,
Period::TN => self - ONE_DAY,
Period::SPOT => self,
Period::SN => self - ONE_DAY,
Period::Days(num) => {
self - Duration::try_days(num).ok_or_else(|| {
Error::PeriodOutOfBounds(format!("{num} days is out of bounds"))
})?
}
Period::Weeks(num) => {
self - Duration::try_days(num * 7).ok_or_else(|| {
Error::PeriodOutOfBounds(format!("{num} weeks is out of bounds"))
})?
}
Period::Months(num) => self - Months::new(num),
Period::Years(num) => self - Months::new(num * 12),
};
Ok(date)
}
}
impl Mul<Period> for u32 {
type Output = Period;
fn mul(self, rhs: Period) -> Self::Output {
match rhs {
Period::ON => Period::ON,
Period::TN => Period::TN,
Period::SPOT => Period::SPOT,
Period::SN => Period::SN,
Period::Days(num) => Period::Days(num * self as i64),
Period::Weeks(num) => Period::Weeks(num * self as i64),
Period::Months(num) => Period::Months(num * self),
Period::Years(num) => Period::Years(num * self),
}
}
}
#[cfg(test)]
mod tests {
use super::Period;
use crate::error::Result;
use chrono::NaiveDate;
#[test]
fn test_settlement_date_target_add() -> Result<()> {
let current_date = NaiveDate::from_ymd_opt(2023, 10, 17).unwrap();
assert_eq!((current_date + Period::SPOT)?, current_date);
assert_eq!(
(current_date + Period::ON)?,
NaiveDate::from_ymd_opt(2023, 10, 18).unwrap()
);
assert_eq!(
(current_date + Period::TN)?,
NaiveDate::from_ymd_opt(2023, 10, 18).unwrap()
);
assert_eq!(
(current_date + Period::SN)?,
NaiveDate::from_ymd_opt(2023, 10, 18).unwrap()
);
assert_eq!(
(current_date + Period::Days(1))?,
NaiveDate::from_ymd_opt(2023, 10, 18).unwrap()
);
assert_eq!(
(current_date + Period::Weeks(1))?,
NaiveDate::from_ymd_opt(2023, 10, 24).unwrap()
);
assert_eq!(
(current_date + Period::Months(1))?,
NaiveDate::from_ymd_opt(2023, 11, 17).unwrap()
);
assert_eq!(
(current_date + Period::Years(1))?,
NaiveDate::from_ymd_opt(2024, 10, 17).unwrap()
);
Ok(())
}
#[test]
fn test_settlement_date_target_sub() -> Result<()> {
let current_date = NaiveDate::from_ymd_opt(2023, 10, 17).unwrap();
assert_eq!((current_date - Period::SPOT)?, current_date);
assert_eq!(
(current_date - Period::ON)?,
NaiveDate::from_ymd_opt(2023, 10, 16).unwrap()
);
assert_eq!(
(current_date - Period::TN)?,
NaiveDate::from_ymd_opt(2023, 10, 16).unwrap()
);
assert_eq!(
(current_date - Period::SN)?,
NaiveDate::from_ymd_opt(2023, 10, 16).unwrap()
);
assert_eq!(
(current_date - Period::Days(1))?,
NaiveDate::from_ymd_opt(2023, 10, 16).unwrap()
);
assert_eq!(
(current_date - Period::Weeks(1))?,
NaiveDate::from_ymd_opt(2023, 10, 10).unwrap()
);
assert_eq!(
(current_date - Period::Months(1))?,
NaiveDate::from_ymd_opt(2023, 9, 17).unwrap()
);
assert_eq!(
(current_date - Period::Years(1))?,
NaiveDate::from_ymd_opt(2022, 10, 17).unwrap()
);
Ok(())
}
#[test]
fn test_mut() {
assert_eq!(2 * Period::Years(1), Period::Years(2));
assert_eq!(2 * Period::Months(1), Period::Months(2));
assert_eq!(2 * Period::Weeks(1), Period::Weeks(2));
assert_eq!(2 * Period::Days(1), Period::Days(2));
}
#[test]
fn test_tn_settlement_date_gbpusd() -> Result<()> {
use crate::time::calendars::{JointCalendar, UnitedKingdom, UnitedStates};
let valuation_date = NaiveDate::from_ymd_opt(2023, 10, 16).unwrap();
let calendar = JointCalendar::new(vec![
Box::new(UnitedStates::default()),
Box::new(UnitedKingdom::default()),
]);
let tn_settle = Period::TN.settlement_date(valuation_date, &calendar)?;
let spot_settle = Period::SPOT.settlement_date(valuation_date, &calendar)?;
assert_eq!(tn_settle, spot_settle);
assert_eq!(tn_settle, NaiveDate::from_ymd_opt(2023, 10, 18).unwrap());
assert_eq!(
Period::ON.settlement_date(valuation_date, &calendar)?,
NaiveDate::from_ymd_opt(2023, 10, 17).unwrap()
);
Ok(())
}
#[test]
fn test_near_date_gbpusd() -> Result<()> {
use crate::time::calendars::{JointCalendar, UnitedKingdom, UnitedStates};
let valuation_date = NaiveDate::from_ymd_opt(2023, 10, 16).unwrap();
let calendar = JointCalendar::new(vec![
Box::new(UnitedStates::default()),
Box::new(UnitedKingdom::default()),
]);
assert_eq!(
Period::ON.near_date(valuation_date, &calendar)?,
Some(NaiveDate::from_ymd_opt(2023, 10, 16).unwrap())
);
assert_eq!(
Period::TN.near_date(valuation_date, &calendar)?,
Some(NaiveDate::from_ymd_opt(2023, 10, 17).unwrap())
);
assert_eq!(
Period::SN.near_date(valuation_date, &calendar)?,
Some(NaiveDate::from_ymd_opt(2023, 10, 18).unwrap())
);
assert_eq!(Period::Weeks(1).near_date(valuation_date, &calendar)?, None);
assert_eq!(
Period::Months(1).near_date(valuation_date, &calendar)?,
None
);
Ok(())
}
}