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