pub fn monthly_payment(principal: f64, annual_rate: f64, months: u32) -> f64 {
if months == 0 { return 0.0; }
let r = annual_rate / 12.0;
if r == 0.0 { principal / months as f64 }
else {
let n = months as f64;
principal * r * (1.0 + r).powf(n) / ((1.0 + r).powf(n) - 1.0)
}
}
pub fn period(balance_before: f64, annual_rate: f64, payment: f64) -> (f64, f64, f64) {
let interest = balance_before * (annual_rate / 12.0);
let principal_paid = (payment - interest).min(balance_before); let remaining = (balance_before - principal_paid).max(0.0);
(interest, principal_paid, remaining)
}
pub fn total_interest(principal: f64, annual_rate: f64, months: u32) -> f64 {
let p = monthly_payment(principal, annual_rate, months);
(p * months as f64) - principal
}
pub fn schedule(principal: f64, annual_rate: f64, months: u32) -> Vec<(f64, f64, f64)> {
let p = monthly_payment(principal, annual_rate, months);
let mut bal = principal;
let mut out = Vec::with_capacity(months as usize);
for _ in 0..months {
let (i, pr, rem) = period(bal, annual_rate, p);
out.push((i, pr, rem));
bal = rem;
if bal <= 0.0 { break; }
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn payment_100k_7pct_30y() {
assert!((monthly_payment(100_000.0, 0.07, 360) - 665.30).abs() < 0.1);
}
#[test]
fn zero_interest_splits_evenly() {
assert!((monthly_payment(12_000.0, 0.0, 12) - 1_000.0).abs() < 1e-9);
}
#[test]
fn total_interest_positive() {
let ti = total_interest(100_000.0, 0.07, 360);
assert!(ti > 130_000.0 && ti < 140_000.0); }
#[test]
fn schedule_pays_down_to_zero() {
let s = schedule(100_000.0, 0.07, 360);
assert!(s.last().unwrap().2 < 1.0); assert_eq!(s.len(), 360);
}
}