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}