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}