finance_solution/cashflow/future_value_annuity.rs
1#![allow(unused_imports)]
2
3//! **Future value _annuity_ calculations**. Given a series of constant cashflows, a number of periods
4//! such as years, and a fixed interest rate, what is the value of the series at the final payment?
5//!
6//! Timing uses [`crate::PaymentTiming`] (or Excel-style `bool` via [`From`]):
7//! - [`PaymentTiming::EndOfPeriod`] / `false` — ordinary annuity (Excel `type=0`)
8//! - [`PaymentTiming::BeginningOfPeriod`] / `true` — annuity due (Excel `type=1`)
9//!
10//! Prefer the enum in new code; `bool` remains for spreadsheet parity.
11//!
12//! For teaching / debugging, use [`future_value_annuity_solution`], which carries formulas and
13//! related PV/FV fields.
14//!
15//! ## Examples
16//!
17//! Ordinary annuity (end of period) with `bool`:
18//! ```
19//! use finance_solution::future_value_annuity_solution;
20//! let (rate, periods, annuity, due) = (0.034, 10, 500, false);
21//! let fv_ann = future_value_annuity_solution(rate, periods, annuity, due).unwrap();
22//! assert!(fv_ann.future_value().abs() > 5_000.0);
23//! ```
24//!
25//! Same scenario with the enum (preferred):
26//! ```
27//! use finance_solution::{future_value_annuity, PaymentTiming};
28//! let ordinary = future_value_annuity(0.034, 10, 500, PaymentTiming::EndOfPeriod).unwrap();
29//! let due = future_value_annuity(0.034, 10, 500, PaymentTiming::BeginningOfPeriod).unwrap();
30//! // Annuity due compounds one extra period → larger magnitude than ordinary
31//! assert!(due.abs() > ordinary.abs());
32//! ```
33//!
34//! Zero-rate special case (both timings reduce to `-payment * periods`):
35//! ```
36//! use finance_solution::{future_value_annuity, PaymentTiming};
37//! let z = future_value_annuity(0.0, 12, 100, PaymentTiming::EndOfPeriod).unwrap();
38//! assert!((z - (-1_200.0)).abs() < 1e-12);
39//! ```
40//!
41
42// to-do: add "use log::warn;" and helper logs
43
44// Needed for the Rustdoc comments and module.
45use crate::assert_approx_equal;
46use crate::cashflow::*;
47use crate::future_value::future_value;
48use crate::present_value::present_value;
49
50fn check_future_value_annuity_parameters(
51 rate: f64,
52 periods: u32,
53 cashflow: f64,
54) -> crate::FinanceResult<()> {
55 crate::util::error::require_rate_gt_minus_one(rate)?;
56 crate::util::error::require_money("annuity", cashflow)?;
57 if periods == 0 {
58 return Err(crate::FinanceError::InvalidPeriod {
59 period: 0,
60 periods: 0,
61 message: "annuity requires at least one period",
62 });
63 }
64 Ok(())
65}
66
67/// Returns the future value of annuity (a series of constant cashflows) at a constant rate. Returns f64.
68///
69/// The future value annuity formula is:
70///
71/// future value ann = sum( cashflow * (1 + rate)<sup>period</sup> )
72///
73/// or
74///
75/// future value ann = Constant_Cashflow * ((1+periodic_rate)^n -1) / periodic_rate
76///
77/// # Arguments
78/// * `rate` - The rate at which the investment grows or shrinks per period,
79/// expressed as a floating point number. For instance 0.05 would mean 5%. Often appears as
80/// `r` or `i` in formulas.
81/// * `periods` - The number of periods such as quarters or years. Often appears as `n` or `t`.
82/// * `cashflow` - The value of the constant cashflow (aka payment).
83/// * `timing` - [`PaymentTiming`] or `bool` (`false` = end of period / Excel `type=0`).
84///
85/// # Errors
86/// Returns [`crate::FinanceError`] if `rate` is less than or equal to -1.0, money is non-finite,
87/// or `periods` is zero.
88///
89/// # Examples
90/// Ordinary annuity (`bool` Excel style):
91/// ```
92/// use finance_solution::*;
93/// let my_annuity = future_value_annuity(0.034, 5, 500, false).unwrap();
94/// assert_approx_equal!(my_annuity, -2_675.8789282);
95/// ```
96///
97/// Annuity due with the enum (preferred):
98/// ```
99/// use finance_solution::{future_value_annuity, PaymentTiming};
100/// let due = future_value_annuity(0.034, 5, 500, PaymentTiming::BeginningOfPeriod).unwrap();
101/// let ordinary = future_value_annuity(0.034, 5, 500, PaymentTiming::EndOfPeriod).unwrap();
102/// assert!(due.abs() > ordinary.abs());
103/// ```
104///
105/// Solution struct (formulas + related fields):
106/// ```
107/// use finance_solution::*;
108/// let sol = future_value_annuity_solution(0.034, 5, 500, PaymentTiming::EndOfPeriod).unwrap();
109/// let final_answer = sol.future_value();
110/// assert_approx_equal!(final_answer, -2_675.8789282);
111/// ```
112///
113/// Monthly contributions:
114/// ```
115/// # use finance_solution::*;
116/// let rate = 0.021; // 2.1% per month
117/// let periods = 12;
118/// let cashflow = 2_000;
119/// let future_value_ann = future_value_annuity(rate, periods, cashflow, false).unwrap();
120/// assert!(future_value_ann.is_finite());
121/// ```
122pub fn future_value_annuity<T, D>(
123 rate: f64,
124 periods: u32,
125 annuity: T,
126 timing: D,
127) -> crate::FinanceResult<f64>
128where
129 T: Into<f64> + Copy,
130 D: Into<crate::PaymentTiming>,
131{
132 let pmt = annuity.into();
133 let timing = timing.into();
134 check_future_value_annuity_parameters(rate, periods, pmt)?;
135
136 // Ordinary annuity (end): FV = -pmt * ((1+r)^n - 1) / r
137 // Annuity due (beginning): multiply by (1 + r) — one extra period of interest on each payment.
138 // Zero rate: FV = -pmt * n for both timings (limit of the geometric series).
139 let fv_ann = match (rate == 0.0, timing) {
140 (true, _) => -pmt * periods as f64,
141 (false, crate::PaymentTiming::EndOfPeriod) => {
142 -pmt * ((1.0 + rate).powf(periods as f64) - 1.0) / rate
143 }
144 (false, crate::PaymentTiming::BeginningOfPeriod) => {
145 -pmt * (1.0 + rate) * ((1.0 + rate).powf(periods as f64) - 1.0) / rate
146 }
147 };
148 if fv_ann.is_finite() {
149 Ok(fv_ann)
150 } else {
151 Err(crate::FinanceError::NonFinite {
152 field: "future_value_annuity",
153 value: fv_ann,
154 })
155 }
156}
157
158/// Returns the future value of annuity (a series of constant cashflows) at a constant rate. Returns custom solution struct with additional information and functionality.
159///
160/// Related functions:
161/// * To calculate a future value returning an f64, use [`present_value_annuity`].
162/// * To calculate a future value with a varying rate or varying cashflow or both, use [`present_value_annuity_schedule`].
163///
164/// The future value annuity formula is:
165///
166/// future value ann = sum( cashflow * (1 + rate)<sup>period</sup> )
167/// or
168/// future value ann = Constant_Cashflow * ((1+periodic_rate)^n -1) / periodic_rate
169///
170/// # Arguments
171/// * `rate` - The rate at which the investment grows or shrinks per period,
172/// expressed as a floating point number. For instance 0.05 would mean 5%. Often appears as
173/// `r` or `i` in formulas.
174/// * `periods` - The number of periods such as quarters or years. Often appears as `n` or `t`.
175/// * `cashflow` - The value of the constant cashflow (aka payment).
176/// * `timing` - [`PaymentTiming`] or `bool` (`false` = end of period).
177///
178/// # Errors
179/// Same domain failures as [`future_value_annuity`].
180///
181/// # Examples
182/// Future value of a $500 annuity at 3.4% for 10 years (ordinary):
183/// ```
184/// use finance_solution::*;
185/// let my_annuity = future_value_annuity_solution(
186/// 0.034, 10, 500, PaymentTiming::EndOfPeriod
187/// ).unwrap();
188/// assert!(my_annuity.future_value().abs() > 5_000.0);
189/// assert!(!my_annuity.due_at_beginning());
190/// ```
191///
192/// Annuity due variant:
193/// ```
194/// use finance_solution::*;
195/// let due = future_value_annuity_solution(
196/// 0.034, 10, 500, PaymentTiming::BeginningOfPeriod
197/// ).unwrap();
198/// assert!(due.due_at_beginning());
199/// ```
200pub fn future_value_annuity_solution<T, D>(
201 rate: f64,
202 periods: u32,
203 cashflow: T,
204 timing: D,
205) -> crate::FinanceResult<CashflowSolution>
206where
207 T: Into<f64> + Copy,
208 D: Into<crate::PaymentTiming>,
209{
210 let annuity = cashflow.into();
211 let timing = timing.into();
212 let due_at_beginning = timing.is_beginning();
213 let fv = future_value_annuity(rate, periods, annuity, timing)?;
214 let fvann_type = match timing {
215 crate::PaymentTiming::BeginningOfPeriod => CashflowVariable::FutureValueAnnuityDue,
216 crate::PaymentTiming::EndOfPeriod => CashflowVariable::FutureValueAnnuity,
217 };
218
219 let (formula, formula_symbolic) = match timing {
220 crate::PaymentTiming::EndOfPeriod => (
221 format!(
222 "-{} * (((1. + {}).powf({}) - 1.) / {});",
223 annuity, rate, periods, rate
224 ),
225 "-annuity * (((1. + rate).powf(periods) - 1.) / rate);".to_string(),
226 ),
227 crate::PaymentTiming::BeginningOfPeriod => (
228 format!(
229 "-{} * (1. + {}) * (((1. + {}).powf({}) - 1.) / {});",
230 annuity, rate, rate, periods, rate
231 ),
232 "-annuity * (1. + rate) * (((1. + rate).powf(periods) - 1.) / rate);".to_string(),
233 ),
234 };
235 let pv = present_value(rate, periods, fv, false)?;
236 Ok(CashflowSolution::new(
237 fvann_type,
238 rate,
239 periods,
240 pv,
241 fv,
242 due_at_beginning,
243 annuity,
244 &formula,
245 &formula_symbolic,
246 ))
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::*;
253
254 #[test]
255 fn test_future_value_annuity() {
256 let rate = 0.034;
257 let periods = 10;
258 let annuity = 500;
259 let fv = future_value_annuity(rate, periods, annuity, false).unwrap();
260 // assert_approx_equal!(5838.66016, fv);
261 assert_eq!(-5838.66016, (fv * 100000.).round() / 100000.);
262 }
263
264 #[test]
265 fn test_future_value_annuity_1() {
266 let rate = 0.034;
267 let periods = 1;
268 let annuity = 500;
269 let fv = future_value_annuity(rate, periods, annuity, false).unwrap();
270 // assert_approx_equal!(5838.66016, fv);
271 assert_eq!(-500.0000, (fv * 100000.).round() / 100000.);
272 }
273 #[test]
274 fn test_future_value_annuity_2() {
275 let rate = 0.034;
276 let periods = 400;
277 let annuity = 500;
278 let fv = future_value_annuity(rate, periods, annuity, false).unwrap();
279 // assert_approx_equal!(9455966284.4844600, fv);
280 assert_eq!(-9455966284.4844600, (fv * 100000.).round() / 100000.);
281 }
282
283 #[test]
284 fn test_future_value_annuity_3() {
285 // big rate
286 let rate = 0.989;
287 let periods = 8;
288 let annuity = 120_000;
289 let fv = future_value_annuity(rate, periods, annuity, false).unwrap();
290 assert_eq!(-29_599_651.75013, (fv * 100000.).round() / 100000.);
291 }
292
293 #[test]
294 fn test_future_value_annuity_4() {
295 let rate = 0.00009;
296 let periods = 780;
297 let annuity = 120_000;
298 let fv = future_value_annuity(rate, periods, annuity, false).unwrap();
299 assert_eq!(-96_959_087.75951, (fv * 100000.).round() / 100000.);
300 }
301
302 #[test]
303 fn test_future_value_annuity_5() {
304 // negative rate
305 let rate = -0.0314;
306 let periods = 10;
307 let annuity = 13_000;
308 let fv = future_value_annuity(rate, periods, annuity, false).unwrap();
309 assert_eq!(-113_087.68194, (fv * 100000.).round() / 100000.);
310 }
311
312 #[test]
313 fn test_future_value_annuity_6() {
314 // big negative rate
315 let rate = -0.999;
316 let periods = 10;
317 let annuity = 13_000;
318 let fv = future_value_annuity(rate, periods, annuity, false).unwrap();
319 assert_eq!(-13_013.01301, (fv * 100000.).round() / 100000.);
320 }
321
322 #[test]
323 fn test_future_value_annuity_7() {
324 // big negative rate, big periods
325 // note: the convergence with the previous test
326 let rate = -0.999;
327 let periods = 780;
328 let annuity = 13_000;
329 let fv = future_value_annuity(rate, periods, annuity, false).unwrap();
330 assert_eq!(-13_013.01301, (fv * 100000.).round() / 100000.);
331 }
332
333 #[test]
334 fn test_future_value_annuity_payment_timing_parity() {
335 use crate::PaymentTiming;
336 let ordinary_bool = future_value_annuity(0.034, 10, 500, false).unwrap();
337 let ordinary_enum =
338 future_value_annuity(0.034, 10, 500, PaymentTiming::EndOfPeriod).unwrap();
339 assert_eq!(ordinary_bool, ordinary_enum);
340
341 let due_bool = future_value_annuity(0.034, 10, 500, true).unwrap();
342 let due_enum =
343 future_value_annuity(0.034, 10, 500, PaymentTiming::BeginningOfPeriod).unwrap();
344 assert_eq!(due_bool, due_enum);
345 assert!(due_enum.abs() > ordinary_enum.abs());
346 }
347}