use crate::ExpirationDate;
use crate::conventions::{Actual365Fixed, DayCount};
use crate::error::ExpirationDateError;
use chrono::{DateTime, Duration, Utc};
use positive::Positive;
use positive::constants::DAYS_IN_A_YEAR;
use std::fmt;
impl ExpirationDate {
#[must_use = "Calculated years must be used to ensure valid financial models."]
pub fn get_years_with_convention<C: DayCount>(
&self,
convention: C,
) -> Result<Positive, ExpirationDateError> {
let now = Utc::now();
let target_date = self.get_date_with_base(now)?;
if target_date <= now {
return Ok(Positive::ZERO);
}
let fraction = convention.year_fraction(&now, &target_date)?;
Positive::new(fraction).map_err(Into::into)
}
#[must_use = "Calculated years must be used to ensure valid financial models."]
#[inline]
pub fn get_years(&self) -> Result<Positive, ExpirationDateError> {
match self {
Self::Days(days) => {
let years = days.to_f64() / DAYS_IN_A_YEAR.to_f64();
Positive::new(years).map_err(Into::into)
}
Self::DateTime(_) => self.get_years_with_convention(Actual365Fixed),
}
}
#[must_use = "Calculated days must be used to ensure valid financial models."]
#[inline]
pub fn get_days(&self) -> Result<Positive, ExpirationDateError> {
match self {
Self::Days(days) => Ok(*days),
Self::DateTime(dt) => {
Self::set_reference_datetime(Some(*dt));
let now = Utc::now();
let duration = dt.signed_duration_since(now);
let num_days = duration.num_seconds() as f64 / 86400.0;
if num_days <= 0.0 {
return Ok(Positive::ZERO);
}
Positive::new(num_days).map_err(Into::into)
}
}
}
#[must_use = "Resolved date must be used for settlement or further calculations."]
#[inline]
pub fn get_date(&self) -> Result<DateTime<Utc>, ExpirationDateError> {
self.get_date_with_options(false)
}
pub(crate) fn get_date_with_base(
&self,
now: DateTime<Utc>,
) -> Result<DateTime<Utc>, ExpirationDateError> {
match self {
Self::Days(days) => {
let base = Self::get_reference_datetime().unwrap_or(now);
Ok(base + Duration::days((*days).to_i64()))
}
Self::DateTime(dt) => Ok(*dt),
}
}
#[must_use = "Resolved date must be used for settlement or further calculations."]
pub fn get_date_with_options(
&self,
use_fixed_time: bool,
) -> Result<DateTime<Utc>, ExpirationDateError> {
if use_fixed_time {
let today = Utc::now().date_naive();
let fixed = today
.and_hms_opt(18, 30, 0)
.ok_or_else(|| ExpirationDateError::InvalidDateTime("Fixed time error".into()))?;
let base_dt = DateTime::<Utc>::from_naive_utc_and_offset(fixed, Utc);
if let Self::Days(days) = self {
return Ok(base_dt + Duration::days((*days).to_i64()));
}
}
self.get_date_with_base(Utc::now())
}
#[must_use = "Formatted string should be consumed for display or reporting."]
pub fn get_date_string(&self) -> Result<String, ExpirationDateError> {
let date = self.get_date_with_options(true)?;
Ok(date.format("%Y-%m-%d").to_string())
}
}
impl Default for ExpirationDate {
#[inline]
fn default() -> Self {
Self::Days(DAYS_IN_A_YEAR)
}
}
impl fmt::Display for ExpirationDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.get_date() {
Ok(dt) => write!(f, "{}", dt.format("%Y-%m-%d %H:%M:%S UTC")),
Err(_) => write!(f, "Invalid Expiration"),
}
}
}