#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::error::Error;
use std::fmt;
use std::fmt::{Display, Formatter};
#[cfg(feature = "chrono")]
mod chrono;
#[cfg(feature = "jiff")]
mod jiff;
const MAX_ERROR: f64 = 1e-10;
const MAX_COMPUTE_WITH_GUESS_ITERATIONS: u32 = 50;
#[derive(Copy, Clone)]
pub struct Payment<T: PaymentDate> {
pub amount: f64,
pub date: T,
}
pub fn compute<T: PaymentDate>(payments: &Vec<Payment<T>>) -> Result<f64, InvalidPaymentsError> {
validate(payments)?;
let mut sorted: Vec<_> = payments.iter().collect();
sorted.sort_by_key(|p| &p.date);
let mut rate = compute_with_guess(&sorted, 0.1);
let mut guess = -0.99;
while guess < 1.0 && (rate.is_nan() || rate.is_infinite()) {
rate = compute_with_guess(&sorted, guess);
guess += 0.01;
}
Ok(rate)
}
#[derive(Debug)]
pub struct InvalidPaymentsError;
impl Display for InvalidPaymentsError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
"negative and positive payments are required".fmt(f)
}
}
impl Error for InvalidPaymentsError {}
fn compute_with_guess<T: PaymentDate>(payments: &Vec<&Payment<T>>, guess: f64) -> f64 {
let mut r = guess;
let mut e = 1.0;
for _ in 0..MAX_COMPUTE_WITH_GUESS_ITERATIONS {
if e <= MAX_ERROR {
return r;
}
let r1 = r - xirr(payments, r) / dxirr(payments, r);
e = (r1 - r).abs();
r = r1;
}
f64::NAN
}
fn xirr<T: PaymentDate>(payments: &Vec<&Payment<T>>, rate: f64) -> f64 {
let mut result = 0.0;
for p in payments {
let exp = get_exp(p, payments[0]);
result += p.amount / (1.0 + rate).powf(exp)
}
result
}
fn dxirr<T: PaymentDate>(payments: &Vec<&Payment<T>>, rate: f64) -> f64 {
let mut result = 0.0;
for p in payments {
let exp = get_exp(p, payments[0]);
result -= p.amount * exp / (1.0 + rate).powf(exp + 1.0)
}
result
}
fn validate<T: PaymentDate>(payments: &Vec<Payment<T>>) -> Result<(), InvalidPaymentsError> {
let positive = payments.iter().any(|p| p.amount > 0.0);
let negative = payments.iter().any(|p| p.amount < 0.0);
if positive && negative {
Ok(())
} else {
Err(InvalidPaymentsError)
}
}
fn get_exp<T: PaymentDate>(p: &Payment<T>, p0: &Payment<T>) -> f64 {
p.date.days_since(p0.date) as f64 / 365.0
}
pub trait PaymentDate: Ord + Sized + Copy {
fn days_since(self, other: Self) -> i32;
}