use std::fmt;
pub type FinanceResult<T> = Result<T, FinanceError>;
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq)]
pub enum FinanceError {
NonFinite { field: &'static str, value: f64 },
InvalidRate { rate: f64 },
InvalidPeriod {
period: u32,
periods: u32,
message: &'static str,
},
ZeroValue { field: &'static str },
SameSignValues {
present_value: f64,
future_value: f64,
},
Unsolvable { message: &'static str },
InvalidCashflow { message: &'static str },
EmptyInput { what: &'static str },
LengthMismatch {
left: usize,
right: usize,
context: &'static str,
},
}
impl FinanceError {
pub fn code(&self) -> &'static str {
match self {
FinanceError::NonFinite { .. } => "non_finite",
FinanceError::InvalidRate { .. } => "invalid_rate",
FinanceError::InvalidPeriod { .. } => "invalid_period",
FinanceError::ZeroValue { .. } => "zero_value",
FinanceError::SameSignValues { .. } => "same_sign_values",
FinanceError::Unsolvable { .. } => "unsolvable",
FinanceError::InvalidCashflow { .. } => "invalid_cashflow",
FinanceError::EmptyInput { .. } => "empty_input",
FinanceError::LengthMismatch { .. } => "length_mismatch",
}
}
}
impl fmt::Display for FinanceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FinanceError::NonFinite { field, value } => {
write!(
f,
"{field} must be finite (not NaN or infinity); got {value}"
)
}
FinanceError::InvalidRate { rate } => {
write!(
f,
"rate is outside the allowed domain for this formula; got {rate}"
)
}
FinanceError::InvalidPeriod {
period,
periods,
message,
} => {
write!(f, "{message} (period={period}, periods={periods})")
}
FinanceError::ZeroValue { field } => {
write!(f, "{field} must be nonzero for this calculation")
}
FinanceError::SameSignValues {
present_value,
future_value,
} => {
write!(
f,
"present_value ({present_value}) and future_value ({future_value}) must have opposite signs"
)
}
FinanceError::Unsolvable { message } => write!(f, "{message}"),
FinanceError::InvalidCashflow { message } => write!(f, "{message}"),
FinanceError::EmptyInput { what } => write!(f, "{what} must not be empty"),
FinanceError::LengthMismatch {
left,
right,
context,
} => {
write!(f, "{context}: length mismatch ({left} vs {right})")
}
}
}
}
impl std::error::Error for FinanceError {}
pub(crate) fn require_finite(field: &'static str, value: f64) -> FinanceResult<()> {
if value.is_finite() {
Ok(())
} else {
Err(FinanceError::NonFinite { field, value })
}
}
pub(crate) fn require_rate(rate: f64) -> FinanceResult<()> {
require_finite("rate", rate)?;
if rate < -1.0 {
Err(FinanceError::InvalidRate { rate })
} else {
Ok(())
}
}
pub(crate) fn require_rate_gt_minus_one(rate: f64) -> FinanceResult<()> {
require_finite("rate", rate)?;
if rate <= -1.0 {
Err(FinanceError::InvalidRate { rate })
} else {
Ok(())
}
}
pub(crate) fn require_money(field: &'static str, value: f64) -> FinanceResult<()> {
require_finite(field, value)
}
pub(crate) fn require_positive(field: &'static str, value: f64) -> FinanceResult<()> {
require_finite(field, value)?;
if value <= 0.0 {
Err(FinanceError::InvalidCashflow {
message: "value must be strictly positive",
})
} else {
Ok(())
}
}
pub(crate) fn require_nonempty<T>(what: &'static str, items: &[T]) -> FinanceResult<()> {
if items.is_empty() {
Err(FinanceError::EmptyInput { what })
} else {
Ok(())
}
}
pub(crate) fn require_rates(rates: &[f64]) -> FinanceResult<()> {
for &rate in rates {
require_rate(rate)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::future_value;
#[test]
fn display_and_code_invalid_rate() {
let err = FinanceError::InvalidRate { rate: -1.5 };
assert!(err.to_string().contains("-1.5"));
assert_eq!(err.code(), "invalid_rate");
}
#[test]
fn future_value_err_invalid_rate() {
match future_value(-1.5, 12, 1000.0, false) {
Err(FinanceError::InvalidRate { rate }) => assert_eq!(rate, -1.5),
other => panic!("unexpected {other:?}"),
}
}
#[test]
fn require_rates_empty_ok() {
assert!(require_rates(&[]).is_ok());
}
#[test]
fn require_rates_invalid() {
assert!(matches!(
require_rates(&[-1.5]),
Err(FinanceError::InvalidRate { .. })
));
}
}