pub fn calc_xnpv(rate: f64, values: &[f64], dates: &[f64]) -> Option<f64> {
if values.is_empty() || values.len() != dates.len() {
return None;
}
if rate <= -1.0 {
return None;
}
let d0 = dates[0].trunc();
if dates.iter().any(|d| d.trunc() < d0) {
return None;
}
Some(xnpv_raw(rate, values, dates, d0))
}
pub(crate) fn xnpv_raw(rate: f64, values: &[f64], dates: &[f64], d0: f64) -> f64 {
values
.iter()
.zip(dates.iter())
.map(|(p, d)| {
let exp = (d.trunc() - d0) / 365.0;
p / (1.0 + rate).powf(exp)
})
.sum()
}
#[cfg(test)]
mod tests {
use super::calc_xnpv;
#[test]
fn xnpv_matches_excel_example() {
let values = [-10000., 2750., 4250., 3250., 2750.];
let dates = [39448., 39508., 39751., 39859., 39904.];
let r = calc_xnpv(0.09, &values, &dates).unwrap();
assert!((r - 2086.6476).abs() < 1e-3, "got {}", r);
}
#[test]
fn xnpv_rejects_bad_input() {
assert!(calc_xnpv(0.1, &[1., 2.], &[100.]).is_none()); assert!(calc_xnpv(-1.5, &[1., 2.], &[100., 200.]).is_none()); assert!(calc_xnpv(0.1, &[1., 2.], &[200., 100.]).is_none()); }
}