rust_finprim/tvm/
fv.rs

1use crate::{ONE, ZERO};
2use rust_decimal::prelude::*;
3
4/// FV - Future Value
5///
6/// A general future value calculation, similar to the Excel `FV` function.
7///
8///
9/// The future value (FV) is the value of an asset or cash at a specified date in the future based on a certain rate of return.
10/// The future value is the amount of money that an investment made today will grow to by a future date.
11/// It is calculated by applying a rate of return to the initial investment over a specified period of time.
12///
13/// # Arguments
14/// * `rate` - The interest rate per period
15/// * `nper` - The number of compounding periods
16/// * `pmt` - The payment amount per period
17/// * `pv` (optional) - The present value, default is 0
18/// * `due` (optional) - The timing of the payment (false = end of period, true = beginning of period), default is false
19/// (ordinary annuity)
20///
21/// At least one of `pmt` or `pv` should be non-zero.
22///
23/// # Returns
24/// * The future value (FV)
25///
26/// # Example
27/// * 5% interest rate
28/// * 10 compounding periods
29/// * $100 payment per period
30/// ```
31/// use rust_finprim::tvm::fv;
32/// use rust_decimal_macros::*;
33///
34/// let rate = dec!(0.05); let nper = dec!(10); let pmt = dec!(-100);
35/// fv(rate, nper, pmt, None, None);
36/// ```
37pub fn fv(rate: Decimal, nper: Decimal, pmt: Decimal, pv: Option<Decimal>, due: Option<bool>) -> Decimal {
38    let pv = pv.unwrap_or(ZERO);
39    let due = due.unwrap_or(false);
40
41    if rate == ZERO {
42        // Simplified formula when rate is zero
43        return pmt * nper + pv;
44    }
45
46    let nth_power = (ONE + rate).powd(nper);
47    let factor = (ONE - nth_power) / rate;
48    let pv_grown = pv * nth_power;
49
50    if due {
51        pmt * factor * (ONE + rate) + pv_grown
52    } else {
53        pmt * factor + pv_grown
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    #[cfg(not(feature = "std"))]
61    extern crate std;
62    use rust_decimal_macros::*;
63    #[cfg(not(feature = "std"))]
64    use std::assert;
65    #[cfg(not(feature = "std"))]
66    use std::prelude::v1::*;
67
68    #[test]
69    fn test_fv() {
70        struct TestCase {
71            rate: Decimal,
72            nper: Decimal,
73            pmt: Decimal,
74            pv: Option<Decimal>,
75            due: Option<bool>,
76            expected: Decimal,
77            description: &'static str,
78        }
79        impl TestCase {
80            fn new(
81                rate: f64,
82                nper: f64,
83                pmt: f64,
84                pv: Option<f64>,
85                due: Option<bool>,
86                expected: f64,
87                description: &'static str,
88            ) -> TestCase {
89                TestCase {
90                    rate: Decimal::from_f64(rate).unwrap(),
91                    nper: Decimal::from_f64(nper).unwrap(),
92                    pmt: Decimal::from_f64(pmt).unwrap(),
93                    pv: pv.map(Decimal::from_f64).unwrap_or(None),
94                    due,
95                    expected: Decimal::from_f64(expected).unwrap(),
96                    description,
97                }
98            }
99        }
100
101        let cases = [
102            TestCase::new(
103                0.05,
104                10.0,
105                -100.0,
106                None,
107                None,
108                1257.78925,
109                "Standard case with 5% rate, 10 periods, and $100 pmt",
110            ),
111            TestCase::new(
112                0.05,
113                10.0,
114                -100.0,
115                None,
116                Some(true),
117                1320.67872,
118                "Payment at the beg of period should result in higher future value",
119            ),
120            TestCase::new(0.0, 10.0, -100.0, None, None, -1000.0, "Zero interest rate no growth"),
121            TestCase::new(
122                0.05,
123                10.0,
124                -100.0,
125                Some(1000.0),
126                None,
127                2886.68388,
128                "Initial investment should result in higher future value",
129            ),
130        ];
131
132        for case in &cases {
133            let calculated_fv = fv(case.rate, case.nper, case.pmt, case.pv, case.due);
134            assert!(
135                (calculated_fv - case.expected).abs() < dec!(1e-5),
136                "Failed on case: {}. Expected {}, got {}",
137                case.description,
138                case.expected,
139                calculated_fv
140            );
141        }
142    }
143}