use crate::error::{NumRs2Error, Result};
use num_traits::Float;
use std::fmt::Debug;
pub fn ipmt<T>(rate: T, per: usize, nper: usize, pv: T, fv: T, when: u8) -> Result<T>
where
T: Float + Debug + Clone,
{
if per < 1 || per > nper {
return Err(NumRs2Error::ValueError(format!(
"Period {} is out of range [1, {}]",
per, nper
)));
}
let nper_t = T::from(nper).expect("Failed to convert nper to type T");
let when_t = T::from(when).expect("Failed to convert when to type T");
let pmt = calculate_pmt(rate, nper_t, pv, fv, when_t)?;
let per_t = T::from(per).expect("Failed to convert per to type T");
let per_minus_1 = T::from(per - 1).expect("Failed to convert per-1 to type T");
let balance = if rate.is_zero() {
pv + pmt * per_minus_1
} else {
let factor = (T::one() + rate).powf(per_minus_1);
pv * factor + pmt * (factor - T::one()) / rate * (T::one() + rate * when_t)
};
let interest = if when == 1 && per == 1 {
T::zero() } else {
-balance * rate };
Ok(interest)
}
pub fn ppmt<T>(rate: T, per: usize, nper: usize, pv: T, fv: T, when: u8) -> Result<T>
where
T: Float + Debug + Clone,
{
let nper_t = T::from(nper).expect("Failed to convert nper to type T");
let when_t = T::from(when).expect("Failed to convert when to type T");
let pmt = calculate_pmt(rate, nper_t, pv, fv, when_t)?;
let interest = ipmt(rate, per, nper, pv, fv, when)?;
Ok(pmt - interest)
}
pub fn cumipmt<T>(
rate: T,
nper: usize,
pv: T,
start_period: usize,
end_period: usize,
when: u8,
) -> Result<T>
where
T: Float + Debug + Clone,
{
if start_period < 1 || start_period > end_period || end_period > nper {
return Err(NumRs2Error::ValueError(format!(
"Invalid period range [{}, {}] for nper={}",
start_period, end_period, nper
)));
}
let mut total_interest = T::zero();
for per in start_period..=end_period {
total_interest = total_interest + ipmt(rate, per, nper, pv, T::zero(), when)?;
}
Ok(total_interest)
}
pub fn cumprinc<T>(
rate: T,
nper: usize,
pv: T,
start_period: usize,
end_period: usize,
when: u8,
) -> Result<T>
where
T: Float + Debug + Clone,
{
if start_period < 1 || start_period > end_period || end_period > nper {
return Err(NumRs2Error::ValueError(format!(
"Invalid period range [{}, {}] for nper={}",
start_period, end_period, nper
)));
}
let mut total_principal = T::zero();
for per in start_period..=end_period {
total_principal = total_principal + ppmt(rate, per, nper, pv, T::zero(), when)?;
}
Ok(total_principal)
}
fn calculate_pmt<T>(rate: T, nper: T, pv: T, fv: T, when: T) -> Result<T>
where
T: Float + Debug,
{
if rate.is_zero() {
Ok(-(pv + fv) / nper)
} else {
let factor = (T::one() + rate).powf(nper);
let denominator = (factor - T::one()) / rate * (T::one() + rate * when);
Ok(-(pv * factor + fv) / denominator)
}
}
pub fn effect<T>(nominal_rate: T, nper: usize) -> Result<T>
where
T: Float + Debug,
{
if nper == 0 {
return Err(NumRs2Error::ValueError(
"Number of compounding periods must be positive".to_string(),
));
}
let nper_t = T::from(nper).expect("Failed to convert nper to type T");
let periodic_rate = nominal_rate / nper_t;
Ok((T::one() + periodic_rate).powf(nper_t) - T::one())
}
pub fn nominal<T>(effective_rate: T, nper: usize) -> Result<T>
where
T: Float + Debug,
{
if nper == 0 {
return Err(NumRs2Error::ValueError(
"Number of compounding periods must be positive".to_string(),
));
}
let nper_t = T::from(nper).expect("Failed to convert nper to type T");
let periodic_rate = (T::one() + effective_rate).powf(T::one() / nper_t) - T::one();
Ok(periodic_rate * nper_t)
}
pub fn sln<T>(cost: T, salvage: T, life: T) -> Result<T>
where
T: Float + Debug,
{
if life <= T::zero() {
return Err(NumRs2Error::ValueError("Life must be positive".to_string()));
}
Ok((cost - salvage) / life)
}
pub fn syd<T>(cost: T, salvage: T, life: usize, per: usize) -> Result<T>
where
T: Float + Debug,
{
if life == 0 {
return Err(NumRs2Error::ValueError("Life must be positive".to_string()));
}
if per < 1 || per > life {
return Err(NumRs2Error::ValueError(format!(
"Period {} is out of range [1, {}]",
per, life
)));
}
let life_t = T::from(life).expect("Failed to convert life to type T");
let per_t = T::from(per).expect("Failed to convert per to type T");
let sum_of_years =
life_t * (life_t + T::one()) / T::from(2.0).expect("Failed to convert 2.0 to type T");
let remaining = life_t - per_t + T::one();
Ok((cost - salvage) * remaining / sum_of_years)
}
pub fn db<T>(cost: T, salvage: T, life: usize, period: usize, month: usize) -> Result<T>
where
T: Float + Debug,
{
if life == 0 {
return Err(NumRs2Error::ValueError("Life must be positive".to_string()));
}
if period < 1 || period > life + 1 {
return Err(NumRs2Error::ValueError(format!(
"Period {} is out of range [1, {}]",
period,
life + 1
)));
}
let life_t = T::from(life).expect("Failed to convert life to type T");
let month_t = T::from(month).expect("Failed to convert month to type T");
let rate = T::one() - (salvage / cost).powf(T::one() / life_t);
let thousand = T::from(1000.0).expect("Failed to convert 1000.0 to type T");
let rate = (rate * thousand).round() / thousand;
let mut book_value = cost;
let twelve = T::from(12.0).expect("Failed to convert 12.0 to type T");
for p in 1..=period {
let depreciation = if p == 1 {
book_value * rate * month_t / twelve
} else if p == life + 1 {
let remaining = book_value - salvage;
if remaining > T::zero() {
remaining
} else {
T::zero()
}
} else {
book_value * rate
};
if p == period {
return Ok(depreciation);
}
book_value = book_value - depreciation;
}
Ok(T::zero())
}
pub fn ddb<T>(cost: T, salvage: T, life: usize, period: usize, factor: T) -> Result<T>
where
T: Float + Debug,
{
if life == 0 {
return Err(NumRs2Error::ValueError("Life must be positive".to_string()));
}
if period < 1 || period > life {
return Err(NumRs2Error::ValueError(format!(
"Period {} is out of range [1, {}]",
period, life
)));
}
let life_t = T::from(life).expect("Failed to convert life to type T");
let rate = factor / life_t;
let mut book_value = cost;
let mut depreciation = T::zero();
for p in 1..=period {
let max_depreciation = book_value * rate;
let allowed = book_value - salvage;
depreciation = if allowed > T::zero() {
max_depreciation.min(allowed)
} else {
T::zero()
};
if p == period {
return Ok(depreciation);
}
book_value = book_value - depreciation;
}
Ok(depreciation)
}
pub fn amortization_schedule<T>(
principal: T,
rate: T,
nper: usize,
) -> Result<AmortizationSchedule<T>>
where
T: Float + Debug + Clone,
{
if nper == 0 {
return Err(NumRs2Error::ValueError(
"Number of periods must be positive".to_string(),
));
}
let nper_t = T::from(nper).expect("Failed to convert nper to type T");
let pmt = calculate_pmt(rate, nper_t, principal, T::zero(), T::zero())?;
let mut periods = Vec::with_capacity(nper);
let mut payments = Vec::with_capacity(nper);
let mut principals = Vec::with_capacity(nper);
let mut interests = Vec::with_capacity(nper);
let mut balances = Vec::with_capacity(nper);
let mut balance = principal;
for per in 1..=nper {
let interest = balance * rate;
let principal_payment = -pmt - interest;
balance = balance - principal_payment;
if balance < T::zero() {
balance = T::zero();
}
periods.push(per);
payments.push(pmt);
principals.push(-principal_payment);
interests.push(-interest);
balances.push(balance);
}
Ok(AmortizationSchedule {
periods,
payments,
principals,
interests,
balances,
})
}
#[derive(Debug, Clone)]
pub struct AmortizationSchedule<T> {
pub periods: Vec<usize>,
pub payments: Vec<T>,
pub principals: Vec<T>,
pub interests: Vec<T>,
pub balances: Vec<T>,
}
impl<T: Float + Clone> AmortizationSchedule<T> {
pub fn total_interest(&self) -> T {
self.interests.iter().fold(T::zero(), |acc, &x| acc + x)
}
pub fn total_payments(&self) -> T {
self.payments.iter().fold(T::zero(), |acc, &x| acc + x)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_ipmt() {
let rate = 0.085 / 12.0;
let nper = 48;
let pv = 2500.0;
let interest = ipmt(rate, 1, nper, pv, 0.0, 0).expect("ipmt calculation should succeed");
assert_relative_eq!(interest, -17.708333, epsilon = 0.01);
}
#[test]
fn test_ppmt() {
let rate = 0.085 / 12.0;
let nper = 48;
let pv = 2500.0;
let principal = ppmt(rate, 1, nper, pv, 0.0, 0).expect("ppmt calculation should succeed");
assert!(principal < 0.0); }
#[test]
fn test_payment_breakdown() {
let rate = 0.08 / 12.0;
let nper = 60;
let pv = 20000.0;
let pmt =
calculate_pmt(rate, nper as f64, pv, 0.0, 0.0).expect("pmt calculation should succeed");
for per in 1..=nper {
let interest =
ipmt(rate, per, nper, pv, 0.0, 0).expect("ipmt calculation should succeed");
let principal =
ppmt(rate, per, nper, pv, 0.0, 0).expect("ppmt calculation should succeed");
assert_relative_eq!(interest + principal, pmt, epsilon = 1e-10);
}
}
#[test]
fn test_cumipmt() {
let rate = 0.09 / 12.0;
let nper = 360;
let pv = 125000.0;
let cum_interest =
cumipmt(rate, nper, pv, 1, 12, 0).expect("cumipmt calculation should succeed");
assert!(cum_interest < 0.0, "Cumulative interest should be negative");
assert!(
cum_interest > -12000.0 && cum_interest < -10000.0,
"First year interest should be between -12000 and -10000, got {}",
cum_interest
);
}
#[test]
fn test_cumprinc() {
let rate = 0.09 / 12.0;
let nper = 360;
let pv = 125000.0;
let cum_principal =
cumprinc(rate, nper, pv, 1, 12, 0).expect("cumprinc calculation should succeed");
assert!(
cum_principal < 0.0,
"Cumulative principal should be negative"
);
let cum_interest =
cumipmt(rate, nper, pv, 1, 12, 0).expect("cumipmt calculation should succeed");
let pmt =
calculate_pmt(rate, nper as f64, pv, 0.0, 0.0).expect("pmt calculation should succeed");
let total_payments = pmt * 12.0;
assert_relative_eq!(
cum_interest + cum_principal,
total_payments,
epsilon = 1e-10
);
}
#[test]
fn test_effect() {
let eff = effect(0.10, 12).expect("effect calculation should succeed");
assert_relative_eq!(eff, 0.10471307, epsilon = 1e-6);
}
#[test]
fn test_nominal() {
let nom = nominal(0.10471307, 12).expect("nominal calculation should succeed");
assert_relative_eq!(nom, 0.10, epsilon = 1e-6);
}
#[test]
fn test_sln() {
let dep = sln(30000.0, 7500.0, 10.0).expect("sln calculation should succeed");
assert_relative_eq!(dep, 2250.0, epsilon = 0.01);
}
#[test]
fn test_syd() {
let dep = syd(30000.0, 7500.0, 10, 1).expect("syd calculation should succeed");
assert_relative_eq!(dep, 4090.909, epsilon = 0.01);
let mut total = 0.0;
for per in 1..=10 {
total += syd(30000.0, 7500.0, 10, per).expect("syd calculation should succeed");
}
assert_relative_eq!(total, 22500.0, epsilon = 0.01);
}
#[test]
fn test_ddb() {
let dep = ddb(2400.0, 300.0, 10, 1, 2.0).expect("ddb calculation should succeed");
assert_relative_eq!(dep, 480.0, epsilon = 0.01);
let dep2 = ddb(2400.0, 300.0, 10, 2, 2.0).expect("ddb calculation should succeed");
assert_relative_eq!(dep2, 384.0, epsilon = 0.01);
}
#[test]
fn test_amortization_schedule() {
let schedule = amortization_schedule(10000.0, 0.05 / 12.0, 12)
.expect("amortization_schedule calculation should succeed");
assert_eq!(schedule.periods.len(), 12);
assert!(
schedule
.balances
.last()
.expect("balances should not be empty")
.abs()
< 0.01
);
let total_principal: f64 = schedule.principals.iter().sum();
assert_relative_eq!(total_principal, -10000.0, epsilon = 0.01);
}
}