amortization_schedule/
lib.rs1pub fn monthly_payment(principal: f64, annual_rate: f64, months: u32) -> f64 {
16 if months == 0 { return 0.0; }
17 let r = annual_rate / 12.0;
18 if r == 0.0 { principal / months as f64 }
19 else {
20 let n = months as f64;
21 principal * r * (1.0 + r).powf(n) / ((1.0 + r).powf(n) - 1.0)
22 }
23}
24
25pub fn period(balance_before: f64, annual_rate: f64, payment: f64) -> (f64, f64, f64) {
27 let interest = balance_before * (annual_rate / 12.0);
28 let principal_paid = (payment - interest).min(balance_before); let remaining = (balance_before - principal_paid).max(0.0);
30 (interest, principal_paid, remaining)
31}
32
33pub fn total_interest(principal: f64, annual_rate: f64, months: u32) -> f64 {
35 let p = monthly_payment(principal, annual_rate, months);
36 (p * months as f64) - principal
37}
38
39pub fn schedule(principal: f64, annual_rate: f64, months: u32) -> Vec<(f64, f64, f64)> {
41 let p = monthly_payment(principal, annual_rate, months);
42 let mut bal = principal;
43 let mut out = Vec::with_capacity(months as usize);
44 for _ in 0..months {
45 let (i, pr, rem) = period(bal, annual_rate, p);
46 out.push((i, pr, rem));
47 bal = rem;
48 if bal <= 0.0 { break; }
49 }
50 out
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 #[test]
57 fn payment_100k_7pct_30y() {
58 assert!((monthly_payment(100_000.0, 0.07, 360) - 665.30).abs() < 0.1);
59 }
60 #[test]
61 fn zero_interest_splits_evenly() {
62 assert!((monthly_payment(12_000.0, 0.0, 12) - 1_000.0).abs() < 1e-9);
63 }
64 #[test]
65 fn total_interest_positive() {
66 let ti = total_interest(100_000.0, 0.07, 360);
67 assert!(ti > 130_000.0 && ti < 140_000.0); }
69 #[test]
70 fn schedule_pays_down_to_zero() {
71 let s = schedule(100_000.0, 0.07, 360);
72 assert!(s.last().unwrap().2 < 1.0); assert_eq!(s.len(), 360);
74 }
75}