amortization-schedule 0.1.0

Loan amortization: monthly payment, per-period breakdown, total interest, remaining balance.
Documentation
//! # amortization-schedule
//!
//! Loan amortization math: fixed-rate monthly payment, per-period interest/principal split,
//! remaining balance, and lifetime interest. Pure Rust, no deps. Same math behind the
//! [AssetLoanCalculator](https://assetloancalculator.com/) amortization tool.
//!
//! ```
//! use amortization_schedule::{monthly_payment, total_interest};
//! let p = monthly_payment(100_000.0, 0.07, 360); // ~665.30
//! let ti = total_interest(100_000.0, 0.07, 360);
//! assert!(p > 0.0 && ti > 0.0);
//! ```

/// Monthly payment on a fully-amortizing fixed-rate loan.
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)
    }
}

/// One period of an amortizing loan: (interest, principal_paid, remaining_balance_after).
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); // don't overpay final
    let remaining = (balance_before - principal_paid).max(0.0);
    (interest, principal_paid, remaining)
}

/// Total interest paid over the life of a fixed-rate amortizing loan.
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
}

/// Full schedule as a Vec of (interest, principal_paid, remaining_balance) per period.
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); // ~139,508
    }
    #[test]
    fn schedule_pays_down_to_zero() {
        let s = schedule(100_000.0, 0.07, 360);
        assert!(s.last().unwrap().2 < 1.0); // remaining ~ 0
        assert_eq!(s.len(), 360);
    }
}