Skip to main content

amortization_schedule/
lib.rs

1//! # amortization-schedule
2//!
3//! Loan amortization math: fixed-rate monthly payment, per-period interest/principal split,
4//! remaining balance, and lifetime interest. Pure Rust, no deps. Same math behind the
5//! [AssetLoanCalculator](https://assetloancalculator.com/) amortization tool.
6//!
7//! ```
8//! use amortization_schedule::{monthly_payment, total_interest};
9//! let p = monthly_payment(100_000.0, 0.07, 360); // ~665.30
10//! let ti = total_interest(100_000.0, 0.07, 360);
11//! assert!(p > 0.0 && ti > 0.0);
12//! ```
13
14/// Monthly payment on a fully-amortizing fixed-rate loan.
15pub 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
25/// One period of an amortizing loan: (interest, principal_paid, remaining_balance_after).
26pub 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); // don't overpay final
29    let remaining = (balance_before - principal_paid).max(0.0);
30    (interest, principal_paid, remaining)
31}
32
33/// Total interest paid over the life of a fixed-rate amortizing loan.
34pub 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
39/// Full schedule as a Vec of (interest, principal_paid, remaining_balance) per period.
40pub 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); // ~139,508
68    }
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); // remaining ~ 0
73        assert_eq!(s.len(), 360);
74    }
75}