use super::validate_financial_params;
use crate::array::Array;
use crate::error::{NumRs2Error, Result};
#[allow(unused_imports)] use num_traits::{Float, One, Zero};
use std::fmt::Debug;
pub fn nper<T>(rate: T, pmt: T, pv: T, fv: T, when: i32) -> Result<T>
where
T: Float + Debug + Clone,
{
validate_financial_params(rate, T::zero(), pmt, pv, fv)?;
let when_factor = if when == 1 { T::one() + rate } else { T::one() };
if rate.is_zero() {
if pmt.is_zero() {
return Err(NumRs2Error::ComputationError(
"Cannot calculate number of periods when both rate and payment are zero"
.to_string(),
));
}
return Ok(-(pv + fv) / pmt);
}
let adjusted_pmt = pmt * when_factor;
if adjusted_pmt.is_zero() {
if pv.is_zero() || (pv < T::zero()) == (fv < T::zero()) {
return Err(NumRs2Error::ComputationError(
"Present value and future value must have opposite signs when payment is zero"
.to_string(),
));
}
let compound_ratio = -fv / pv;
if compound_ratio <= T::zero() {
return Err(NumRs2Error::ComputationError(
"Invalid compound ratio for zero payment case".to_string(),
));
}
return Ok(compound_ratio.ln() / (T::one() + rate).ln());
}
let pmt_fv_term = adjusted_pmt - fv * rate;
let pmt_pv_term = adjusted_pmt + pv * rate;
let epsilon = T::from(1e-15).expect("Failed to convert epsilon value");
if pmt_pv_term.abs() < epsilon {
return Err(NumRs2Error::ComputationError(
"Division by zero in number of periods calculation".to_string(),
));
}
let ratio = pmt_fv_term / pmt_pv_term;
if ratio <= T::zero() {
return Err(NumRs2Error::ComputationError(
"Invalid ratio in number of periods calculation - check cash flow signs".to_string(),
));
}
let log_ratio = ratio.ln();
let log_one_plus_rate = (T::one() + rate).ln();
Ok(log_ratio / log_one_plus_rate)
}
pub fn nper_array<T>(
rate: &Array<T>,
pmt: &Array<T>,
pv: &Array<T>,
fv: &Array<T>,
when: i32,
) -> Result<Array<T>>
where
T: Float + Debug + Clone,
{
if rate.shape() != pmt.shape() || rate.shape() != pv.shape() || rate.shape() != fv.shape() {
return Err(NumRs2Error::DimensionMismatch(
"All input arrays must have the same shape".to_string(),
));
}
let rate_vec = rate.to_vec();
let pmt_vec = pmt.to_vec();
let pv_vec = pv.to_vec();
let fv_vec = fv.to_vec();
let mut result_vec = Vec::with_capacity(rate_vec.len());
for i in 0..rate_vec.len() {
let nper_result = nper(rate_vec[i], pmt_vec[i], pv_vec[i], fv_vec[i], when)?;
result_vec.push(nper_result);
}
Ok(Array::from_vec(result_vec).reshape(&rate.shape()))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_nper_basic_loan() {
let monthly_rate = 0.05 / 12.0;
let result =
nper(monthly_rate, -188.71, 10000.0, 0.0, 0).expect("nper calculation should succeed");
assert_relative_eq!(result, 60.0, epsilon = 1e-2);
}
#[test]
fn test_nper_savings_goal() {
let result = nper(0.05, -100.0, 0.0, 1257.79, 0).expect("nper calculation should succeed");
assert_relative_eq!(result, 10.0, epsilon = 1e-2);
}
#[test]
fn test_nper_zero_payment() {
let result = nper(0.05, 0.0, -1000.0, 1628.89, 0).expect("nper calculation should succeed");
assert_relative_eq!(result, 10.0, epsilon = 1e-2);
}
#[test]
fn test_nper_zero_rate() {
let result = nper(0.0, -100.0, 1000.0, 0.0, 0).expect("nper calculation should succeed");
assert_relative_eq!(result, 10.0, epsilon = 1e-9);
}
#[test]
fn test_nper_beginning_of_period() {
let monthly_rate = 0.05 / 12.0;
let result =
nper(monthly_rate, -188.71, 10000.0, 0.0, 1).expect("nper calculation should succeed");
assert!(result < 60.0 && result > 58.0);
}
#[test]
fn test_nper_with_future_value() {
let result =
nper(0.05, -200.0, 1000.0, 5000.0, 0).expect("nper calculation should succeed");
assert!(result > 0.0); }
#[test]
fn test_nper_array() {
let rates = Array::from_vec(vec![0.05 / 12.0, 0.06 / 12.0]);
let pmts = Array::from_vec(vec![-188.71, -250.0]);
let pvs = Array::from_vec(vec![10000.0, 12000.0]);
let fvs = Array::from_vec(vec![0.0, 0.0]);
let result = nper_array(&rates, &pmts, &pvs, &fvs, 0)
.expect("nper_array calculation should succeed");
assert_eq!(result.shape(), vec![2]);
let values = result.to_vec();
assert_relative_eq!(values[0], 60.0, epsilon = 1e-2);
assert!(values[1] > 0.0); }
#[test]
fn test_nper_invalid_parameters() {
let result = nper(0.0, 0.0, 1000.0, 1000.0, 0);
assert!(result.is_err());
}
}