rust_finprim/amort_dep_tax/
amort.rs

1use crate::amort_dep_tax::AmortizationPeriod;
2use crate::FloatLike;
3use crate::RoundingMode;
4
5#[cfg(feature = "std")]
6/// Amortization Schedule
7///
8/// Calculates the amortization schedule for a loan or mortgage.
9///
10/// The amortization schedule includes a series of payments that are applied to both
11/// principal and interest. Each payment reduces the principal balance and pays interest
12/// charges based on the remaining balance and the interest rate.
13///
14///
15/// # Feature
16/// This function requires the `std` feature to be enabled as it uses `std::Vec`. `amort_schedule_into`
17/// can be used in `no_std` environments as any allocation is done by the caller.
18///
19/// # Arguments
20/// * `rate` - The interest rate per period
21/// * `nper` - The total number of payment periods
22/// * `principal` - The present value or principal amount of the loan (should be positive as cash inflow for a mortgage/loan)
23/// * `pmt` - The payment amount per period (should be negative as cash outflow, can be calculated using `pmt` function)
24/// * `round` (optional) - A tuple specifying the number of decimal places and a rounding
25/// strategy for the amounts `(dp, RoundingMode)`, default is no rounding of calculations. The final principal
26/// payment is adjusted to zero out the remaining balance if rounding is enabled.
27///
28/// # Returns
29/// * A vector of `AmortizationPeriod` instances representing each period in the amortization schedule.
30///
31/// # Examples
32/// * 5% rate, 30 year term (360 months), $1,000,000 loan, $4,000 monthly payment
33/// ```
34/// use rust_finprim::amort_dep_tax::amort_schedule;
35/// use rust_finprim::tvm::pmt;
36///
37/// let rate = 0.05 / 12.0; // Monthly rate
38/// let nper = 30 * 12;
39/// let principal = 1_000_000.0;
40/// let pmt = pmt(rate, nper.into(), principal, None, None);
41///
42/// let schedule = amort_schedule(rate, nper, principal, pmt, None);
43/// ```
44pub fn amort_schedule<T: FloatLike>(
45    rate: T,
46    nper: u32,
47    principal: T,
48    pmt: T,
49    round: Option<(u32, RoundingMode, T)>,
50) -> Vec<AmortizationPeriod<T>> {
51    let mut periods = vec![AmortizationPeriod::default(); nper as usize];
52    amort_schedule_into(periods.as_mut_slice(), rate, principal, pmt, round);
53    periods
54}
55
56/// Amortization Schedule Into
57///
58/// Calculates the amortization schedule for a loan or mortgage, mutating a slice of `AmortizationPeriod`.
59///
60/// The amortization schedule includes a series of payments that are applied to both
61/// principal and interest. Each payment reduces the principal balance and pays interest
62/// charges based on the remaining balance and the interest rate.
63///
64///
65/// # Arguments
66/// * `slice` - A mutable slice of `AmortizationPeriod` instances to be filled with the amortization schedule.
67///
68/// **Warning**: The length of the slice should be as long as the number of periods (e.g.
69/// 30 * 12 for 12 months over 30 years) or there will be unexpected behavior.
70/// * `rate` - The interest rate per period
71/// * `principal` - The present value or principal amount of the loan (should be positive as cash inflow for a mortgage/loan)
72/// * `pmt` - The payment amount per period (should be negative as cash outflow, can be calculated using `pmt` function)
73/// * `round` (optional) - A tuple specifying the number of decimal places and a rounding
74/// strategy for the amounts `(dp, RoundingMode)`, default is no rounding of calculations. The final principal
75/// payment is adjusted to zero out the remaining balance if rounding is enabled.
76///
77/// # Examples
78/// * 5% rate, 30 year term (360 months), $1,000,000 loan, $4,000 monthly payment
79/// ```
80/// use rust_finprim::amort_dep_tax::{AmortizationPeriod, amort_schedule_into};
81/// use rust_finprim::tvm::pmt;
82///
83/// let nper = 30 * 12;
84/// let rate = 0.05 / 12.0;
85/// let principal = 1_000_000.0;
86/// let pmt = pmt(rate, nper.into(), principal, None, None);
87///
88/// let mut schedule = vec![AmortizationPeriod::default(); nper as usize];
89/// amort_schedule_into(schedule.as_mut_slice(), rate, principal, pmt, None);
90/// ```
91///
92/// If you wanted to add an "initial period" to the schedule efficiently, you could
93/// do something like this:
94/// ```
95/// use rust_finprim::amort_dep_tax::{AmortizationPeriod, amort_schedule_into};
96/// use rust_finprim::tvm::pmt;
97///
98/// let nper = 30 * 12;
99/// let rate = 0.05 / 12.0;
100/// let principal = 1_000_000.0;
101/// let pmt = pmt(rate, nper.into(), principal, None, None);
102///
103/// let mut schedule = vec![AmortizationPeriod::default(); nper as usize + 1];
104/// schedule[0] = AmortizationPeriod::new(0, 0.0, 0.0, principal);
105/// amort_schedule_into(&mut schedule[1..], rate, principal, pmt, None);
106pub fn amort_schedule_into<T: FloatLike>(
107    slice: &mut [AmortizationPeriod<T>],
108    rate: T,
109    principal: T,
110    pmt: T,
111    round: Option<(u32, RoundingMode, T)>,
112) {
113    let pmt = if let Some((dp, rounding, epsilon)) = round {
114        -pmt.round_with_mode(dp, rounding, epsilon)
115    } else {
116        -pmt
117    };
118
119    let mut remaining_balance = principal;
120    for (period, item) in slice.iter_mut().enumerate() {
121        let mut interest_payment = remaining_balance * rate;
122        let mut principal_payment = pmt - interest_payment;
123
124        if let Some((dp, rounding, epsilon)) = round {
125            principal_payment = principal_payment.round_with_mode(dp, rounding, epsilon);
126            interest_payment = interest_payment.round_with_mode(dp, rounding, epsilon);
127        }
128
129        remaining_balance -= principal_payment;
130
131        *item = AmortizationPeriod::new(
132            period as u32 + 1,
133            principal_payment,
134            interest_payment,
135            remaining_balance,
136        );
137    }
138
139    // Zero out the final balance when rounding is enabled
140    // by subtracting the remaining balance from the final payment
141    // (adding the remaining balance to the principal payment)
142    if round.is_some() {
143        let final_payment = slice.last_mut().unwrap();
144        final_payment.principal_payment += final_payment.remaining_balance;
145        final_payment.remaining_balance = T::zero();
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[cfg(not(feature = "std"))]
154    extern crate std;
155    #[cfg(not(feature = "std"))]
156    use std::{assert_eq, println};
157
158    #[test]
159    fn test_amort_schedule_into() {
160        let rate = 0.05 / 12.0;
161        const NPER: f64 = 30f64 * 12f64;
162        let principal = 250_000.0;
163        let pmt = crate::tvm::pmt(rate, NPER, principal, None, None);
164        println!("PMT: {}", pmt);
165        let mut schedule = [AmortizationPeriod::default(); NPER as usize];
166        amort_schedule_into(&mut schedule, rate, principal, pmt, None);
167        schedule.iter().for_each(|period| {
168            println!("{:?}", period);
169        });
170        // Check the final balance is close to zero
171        assert!(schedule.last().unwrap().remaining_balance.abs() < 1e-8);
172
173        let mut schedule_round = [AmortizationPeriod::default(); NPER as usize];
174
175        amort_schedule_into(
176            &mut schedule_round,
177            rate,
178            principal,
179            pmt,
180            Some((2, RoundingMode::HalfToEven, 1e-8)),
181        );
182        schedule_round.iter().for_each(|period| {
183            println!("{:?}", period);
184        });
185        // Check the final balance is Zero
186        let sec_last_elem = schedule_round.get(358).unwrap();
187        let last_elem = schedule_round.last().unwrap();
188        assert_eq!(sec_last_elem.remaining_balance - last_elem.principal_payment, 0.0);
189        assert_eq!(last_elem.remaining_balance, 0.0);
190    }
191}