Skip to main content

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}