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