use crate::FloatLike;
pub fn pmt<T: FloatLike>(rate: T, nper: T, pv: T, fv: Option<T>, due: Option<bool>) -> T {
let fv: T = fv.unwrap_or(T::zero());
let due = due.unwrap_or(false);
if rate == T::zero() {
return -(pv + fv) / nper;
}
let nth_power = (T::one() + rate).powf(nper);
let numerator = rate * (-pv * nth_power - fv);
let denominator = if due {
(T::one() - nth_power) * (T::one() + rate)
} else {
T::one() - nth_power
};
-numerator / denominator
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
extern crate std;
#[cfg(not(feature = "std"))]
use std::assert;
#[test]
fn test_pmt() {
struct TestCase {
rate: f64,
nper: f64,
pv: f64,
fv: Option<f64>,
due: Option<bool>,
expected: f64,
description: &'static str,
}
impl TestCase {
fn new(
rate: f64,
nper: f64,
pv: f64,
fv: Option<f64>,
due: Option<bool>,
expected: f64,
description: &'static str,
) -> TestCase {
TestCase {
rate,
nper,
pv,
fv,
due,
expected,
description,
}
}
}
let cases = [
TestCase::new(
0.05,
10.0,
-1000.0,
Some(1000.0),
None,
50.0,
"5% coupon bond with 10 periods and $1000 present value",
),
TestCase::new(
0.05,
10.0,
1000.0,
None,
None,
-129.50457,
"Paying off a $1000 loan with a 5% interest rate",
),
TestCase::new(
0.0,
10.0,
1000.0,
Some(100.0),
None,
-110.0,
"Zero interest rate no growth",
),
TestCase::new(
0.05,
10.0,
1000.0,
Some(100.0),
Some(true),
-130.90955,
"Payment at the beg of period should result in lower payment",
),
TestCase::new(0.05, 10.0, 0.0, Some(1000.0), None, -79.50457, "No PV, just a FV"),
TestCase::new(
0.05,
10.0,
-1100.0,
Some(1000.0),
None,
62.95046,
"10yr bond trading at a premium, 5% YTM, what's my coupon payment?",
),
];
for case in &cases {
let calculated_pmt = pmt(case.rate, case.nper, case.pv, case.fv, case.due);
assert!(
(calculated_pmt - case.expected).abs() < 1e-5,
"Failed on case: {}. Expected {}, got {}",
case.description,
case.expected,
calculated_pmt
);
}
}
}