use crate::time::{Date, Frequency};
use std::error::Error as StdError;
use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DeterminationPeriod {
start: Date,
end: Date,
}
impl DeterminationPeriod {
pub fn new(start: Date, end: Date) -> Result<Self, IcmaError> {
if start >= end {
return Err(IcmaError::InvalidPeriod { start, end });
}
Ok(Self { start, end })
}
#[must_use]
pub const fn start(self) -> Date {
self.start
}
#[must_use]
pub const fn end(self) -> Date {
self.end
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ActualActualIcma {
periods: Vec<DeterminationPeriod>,
frequency: Frequency,
events_per_year: u32,
coverage_start: Date,
coverage_end: Date,
}
impl ActualActualIcma {
pub fn new(periods: Vec<DeterminationPeriod>, frequency: Frequency) -> Result<Self, IcmaError> {
if frequency.period().is_none_or(|period| period.length() <= 0) {
return Err(IcmaError::InvalidFrequency { frequency });
}
let Some(events_per_year) = frequency.events_per_year().filter(|&count| count > 0) else {
return Err(IcmaError::InvalidFrequency { frequency });
};
let Some(first) = periods.first() else {
return Err(IcmaError::EmptyPeriods);
};
let coverage_start = first.start;
let mut coverage_end = first.end;
for (index, period) in periods.iter().enumerate().skip(1) {
if period.start != coverage_end {
return Err(IcmaError::NonContiguousPeriods {
index,
previous_end: coverage_end,
start: period.start,
});
}
coverage_end = period.end;
}
Ok(Self {
periods,
frequency,
events_per_year,
coverage_start,
coverage_end,
})
}
#[must_use]
pub fn periods(&self) -> &[DeterminationPeriod] {
&self.periods
}
#[must_use]
pub const fn frequency(&self) -> Frequency {
self.frequency
}
#[must_use]
pub const fn coverage_start(&self) -> Date {
self.coverage_start
}
#[must_use]
pub const fn coverage_end(&self) -> Date {
self.coverage_end
}
#[must_use]
pub const fn day_count(&self, start: Date, end: Date) -> i32 {
end.days_since(start)
}
pub fn year_fraction(&self, start: Date, end: Date) -> Result<f64, IcmaError> {
let lower = start.min(end);
let upper = start.max(end);
if lower < self.coverage_start || upper > self.coverage_end {
return Err(IcmaError::OutOfCoverage {
start,
end,
coverage_start: self.coverage_start,
coverage_end: self.coverage_end,
});
}
if start == end {
return Ok(0.0);
}
let first = self.periods.partition_point(|period| period.end <= lower);
let mut sum = 0.0_f64;
let mut correction = 0.0_f64;
for period in self.periods.iter().skip(first) {
if period.start >= upper {
break;
}
let overlap_start = lower.max(period.start);
let overlap_end = upper.min(period.end);
let numerator = f64::from(overlap_end.days_since(overlap_start));
let denominator =
f64::from(self.events_per_year) * f64::from(period.end.days_since(period.start));
let term = numerator / denominator;
let next = sum + term;
correction += if sum >= term {
(sum - next) + term
} else {
(term - next) + sum
};
sum = next;
}
let result = sum + correction;
Ok(if start > end { -result } else { result })
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum IcmaError {
#[non_exhaustive]
InvalidPeriod {
start: Date,
end: Date,
},
EmptyPeriods,
#[non_exhaustive]
InvalidFrequency {
frequency: Frequency,
},
#[non_exhaustive]
NonContiguousPeriods {
index: usize,
previous_end: Date,
start: Date,
},
#[non_exhaustive]
OutOfCoverage {
start: Date,
end: Date,
coverage_start: Date,
coverage_end: Date,
},
}
impl IcmaError {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::InvalidPeriod { .. } => "icma.invalid_period",
Self::EmptyPeriods => "icma.empty_periods",
Self::InvalidFrequency { .. } => "icma.invalid_frequency",
Self::NonContiguousPeriods { .. } => "icma.non_contiguous_periods",
Self::OutOfCoverage { .. } => "icma.out_of_coverage",
}
}
}
impl fmt::Display for IcmaError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidPeriod { start, end } => write!(
formatter,
"ICMA determination period must be positive: {start}..{end}"
),
Self::EmptyPeriods => formatter.write_str("ICMA needs explicit determination periods"),
Self::InvalidFrequency { frequency } => write!(
formatter,
"ICMA needs a periodic annual frequency, got {frequency}"
),
Self::NonContiguousPeriods {
index,
previous_end,
start,
} => write!(
formatter,
"ICMA period {index} starts at {start}, expected {previous_end}"
),
Self::OutOfCoverage {
start,
end,
coverage_start,
coverage_end,
} => write!(
formatter,
"ICMA accrual {start}..{end} lies outside {coverage_start}..{coverage_end}"
),
}
}
}
impl StdError for IcmaError {}