Skip to main content

finance_solution/tvm/
periods.rs

1//! **Number of periods calculations.** Given a periodic rate, present value, and future value, find the
2//! number of periods needed to satisfy the equation.
3//!
4//! For most common usages, we recommend the [periods_solution](fn.periods_solution.html) function.
5//!
6//! # Concepts
7//!
8//! Suppose we invest $100 at 10% annual interest. After one year the investment is worth $110.
9//! After two years it's worth $110 plus 10% or $121, and so on:
10//!
11//! <img src="http://i.upmath.me/svg/%24%24%5Cbegin%7Btikzpicture%7D%5Bscale%3D1.0544%5D%5Csmall%0A%5Cbegin%7Baxis%7D%5Baxis%20line%20style%3Dgray%2C%0A%09samples%3D12%2C%0A%09width%3D9.0cm%2Cheight%3D6.4cm%2C%0A%09xmin%3D0%2C%20xmax%3D12%2C%0A%09ymin%3D70%2C%20ymax%3D370%2C%0A%09restrict%20y%20to%20domain%3D0%3A1000%2C%0A%09ytick%3D%7B100%2C%20150%2C%20200%2C%20250%2C%20300%2C%20350%7D%2C%0A%09xtick%3D%7B1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%7D%2C%0A%09axis%20x%20line%3Dcenter%2C%0A%09axis%20y%20line%3Dcenter%2C%0A%09xlabel%3D%24n%24%2Cylabel%3D%24fv%24%5D%0A%5Caddplot%5Bblue%2Cdomain%3D1%3A12%2Csemithick%2Conly%20marks%5D%7B100*((1.1)%5Ex)%7D%3B%0A%5Caddplot%5Bblue%5D%20coordinates%20%7B(5.4%2C110)%7D%20node%7B%24fv%3D100(1.1%5En)%24%7D%3B%0A%5Cpath%20(axis%20cs%3A0%2C122)%20node%20%5Banchor%3Dnorth%20west%2Cyshift%3D-0.07cm%5D%3B%0A%5Cend%7Baxis%7D%0A%5Cend%7Btikzpicture%7D%24%24" />
12//!
13//! Here `n` is the number of periods, in this case years, and `fv` is the future value, or the
14//! value of the investment after some number of years. After 12 years the investment would grow to
15//! a little over $300.
16//!
17//! But suppose our goal is to reach $250 and we need to know exactly how many years that will take.
18//! This is where the periods calculations come in. They find the point where an investment reaches
19//! some fixed value:
20//!
21//! <img src="http://i.upmath.me/svg/%24%24%5Cbegin%7Btikzpicture%7D%5Bscale%3D1.0544%5D%5Csmall%0A%5Cbegin%7Baxis%7D%5Baxis%20line%20style%3Dgray%2C%0A%09samples%3D100%2C%0A%09width%3D9.0cm%2Cheight%3D6.4cm%2C%0A%09xmin%3D0%2C%20xmax%3D12%2C%0A%09ymin%3D70%2C%20ymax%3D370%2C%0A%09restrict%20y%20to%20domain%3D0%3A1000%2C%0A%09ytick%3D%7B100%2C%20150%2C%20200%2C%20250%2C%20300%2C%20350%7D%2C%0A%09xtick%3D%7B1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%7D%2C%0A%09axis%20x%20line%3Dcenter%2C%0A%09axis%20y%20line%3Dcenter%2C%0A%09xlabel%3D%24n%24%2Cylabel%3D%24fv%24%5D%0A%5Caddplot%5Bblue%2Cdomain%3D1%3A9.614%2Cthick%5D%7B100*((1.1)%5Ex)%7D%3B%0A%5Caddplot%5Bblue%2Cdomain%3D9.614%3A12%2Cthick%2Cdashed%5D%7B100*((1.1)%5Ex)%7D%3B%0A%5Caddplot%5Bblack%2Cdomain%3D1%3A12%5D%7B250%7D%3B%0A%5Caddplot%5B%5D%20coordinates%20%7B(2.1%2C%20270)%7D%20node%7B%24fv%3D250%24%7D%3B%0A%5Caddplot%5Bblue%5D%20coordinates%20%7B(5.5%2C120.3)%7D%20node%7B%24fv%3D100(1.1%5En)%24%7D%3B%0A%5Caddplot%5Bred%5D%20coordinates%20%7B(10.6%2C235)%7D%20node%7B%24n%3D9.61%24%7D%3B%0A%5Cpath%20(axis%20cs%3A0%2C122)%20node%20%5Banchor%3Dnorth%20west%2Cyshift%3D-0.07cm%5D%3B%0A%5Cend%7Baxis%7D%0A%5Cend%7Btikzpicture%7D%24%24" />
22//!
23//! Here the investment reaches $250 after 9.61 years.
24//!
25//! The same ideas apply with a negative rate. Suppose we have a value that starts at $100 and
26//! declines by 10% per year. At what point does the value fall to $70?
27//!
28//! <img src="http://i.upmath.me/svg/%24%24%5Cbegin%7Btikzpicture%7D%5Bscale%3D1.0544%5D%5Csmall%0A%5Cbegin%7Baxis%7D%5Baxis%20line%20style%3Dgray%2C%0A%09samples%3D100%2C%0A%09width%3D9.0cm%2Cheight%3D6.4cm%2C%0A%09xmin%3D0%2C%20xmax%3D12%2C%0A%09ymin%3D0%2C%20ymax%3D120%2C%0A%09restrict%20y%20to%20domain%3D0%3A1000%2C%0A%09ytick%3D%7B10%2C%2020%2C%2030%2C%2040%2C%2050%2C%2060%2C%2070%2C80%2C%2090%2C%20100%7D%2C%0A%09xtick%3D%7B1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%7D%2C%0A%09axis%20x%20line%3Dcenter%2C%0A%09axis%20y%20line%3Dcenter%2C%0A%09xlabel%3D%24n%24%2Cylabel%3D%24fv%24%5D%0A%5Caddplot%5Bblue%2Cdomain%3D1%3A3.385%2Cthick%5D%7B100*((0.9)%5Ex)%7D%3B%0A%5Caddplot%5Bblue%2Cdomain%3D3.385%3A12%2Cthick%2Cdashed%5D%7B100*((0.9)%5Ex)%7D%3B%0A%5Caddplot%5Bblack%2Cdomain%3D1%3A12%5D%7B70%7D%3B%0A%5Caddplot%5B%5D%20coordinates%20%7B(11%2C%2064)%7D%20node%7B%24fv%3D70%24%7D%3B%0A%5Caddplot%5Bblue%5D%20coordinates%20%7B(6.8%2C30)%7D%20node%7B%24fv%3D100(0.9%5En)%24%7D%3B%0A%5Caddplot%5Bred%5D%20coordinates%20%7B(4.5%2C75.1)%7D%20node%7B%24n%3D3.39%24%7D%3B%0A%5Cpath%20(axis%20cs%3A0%2C122)%20node%20%5Banchor%3Dnorth%20west%2Cyshift%3D-0.07cm%5D%3B%0A%5Cend%7Baxis%7D%0A%5Cend%7Btikzpicture%7D%24%24" />
29//!
30//! After 3.39 periods the value is $70.
31//!
32//! # Formulas
33//!
34//! ## Simple Compounding
35//!
36//! With simple compound interest the number of periods is calculated with:
37//!
38//! > <img src="http://i.upmath.me/svg/periods%20%3D%20%5Cfrac%7B%5Clog_%7B1%2Brate%7D%5Cleft(%5Cfrac%7Bfuture%5C_value%7D%7Bpresent%5C_value%7D%5Cright)%7D%7Brate%7D" />
39//!
40//! Or using some more common variable names:
41//!
42//! > <img src="http://i.upmath.me/svg/n%20%3D%20%5Cfrac%7B%5Clog_%7B1%2Br%7D%5Cleft(%5Cfrac%7Bfv%7D%7Bpv%7D%5Cright)%7Dr" />
43//!
44//! `n` is often used for the number of periods, though it may be `t` for time if each period is
45//! assumed to be one year as in continuous compounding. `r` is the periodic rate, though this may
46//! appear as `i` for interest.
47//!
48//! Throughout this crate we use `pv` for present value and `fv` for future value. You may see these
49//! values called `P` for principal in some references.
50//!
51//! Within the [TvmSolution](./struct.TvmSolution.html) struct we record the formula used for the particular calculation
52//! using both concrete values and symbols. For the example above with $100 growing at 10%, where we
53//! want to end up with $250 the struct contains:
54//! ```text
55//! formula: "9.61 = log(250.0000 / 100.0000, base 1.100000)",
56//! symbolic_formula: "n = log(-fv / pv, base (1 + r))",
57//! ```
58//!
59//! ## Continuous Compounding
60//!
61//! With continuous compounding it's:
62//!
63//! > <img src="http://i.upmath.me/svg/periods%20%3D%20%5Cfrac%7B%5Cln%5Cleft(%5Cfrac%7Bfuture%5C_value%7D%7Bpresent%5C_value%7D%5Cright)%7D%7Brate%7D" />
64//!
65//! or:
66//!
67//! > <img src="http://i.upmath.me/svg/n%20%3D%20%5Cfrac%7B%5Cln%5Cleft(%5Cfrac%7Bfv%7D%7Bpv%7D%5Cright)%7Dr" />
68//!
69//! With continuous compounding the period is assumed to be years and `t` (time) is often used as
70//! the variable name. Within this crate we stick with `n` for the number of periods so that it's
71//! easier to compare formulas when they're printed as simple text as part of the [TvmSolution](./struct.TvmSolution.html)
72//! struct, as in:
73//! ```text
74//! formula: "9.16 = ln(250.0000 / 100.0000) / 0.100000",
75//! symbolic_formula: "n = ln(fv / pv) / r",
76//! ```
77
78// use log::warn;
79
80use super::tvm::*;
81
82/// Returns the number of periods given a periodic rate along with the present and future values,
83/// using simple compounding.
84///
85/// Note that the returned number of periods will be a floating point number representing fractional
86/// periods.
87///
88/// See the [periods](./index.html) module page for the formulas.
89///
90/// Related functions:
91/// * To calculate the periods using simple compounding and return a struct that shows the formula
92/// and can be used to produce the the period-by-period values use [periods_solution](fn.periods_solution.html).
93/// * To calculate the periods using continuous compounding use [periods_continuous](fn.periods_continuous.html)
94/// or [periods_continuous_solution](fn.periods_continuous_solution.html).
95///
96/// # Arguments
97/// * `rate` - The rate at which the investment grows or shrinks per period, expressed as a
98/// floating point number. For instance 0.05 would mean 5% growth. Often appears as `r` or `i` in
99/// formulas.
100/// * `present_value` - The starting value of the investment. May appear as `pv` in formulas, or `C`
101/// for cash flow or `P` for principal.
102/// * `future_value` - The final value of the investment.
103/// * `continuous_compounding` - True for continuous compounding, false for simple compounding.
104///
105/// # Errors
106/// The call returns [`FinanceError`] if the rate, the present value, or the future value is infinite or not a
107/// number (NaN).
108///
109/// The call will also fail in any of the follwing cases because there is no number of periods that
110/// would make the calculation work:
111/// * The periodic rate is less than -1.0.
112/// * The present value is zero and the future value is nonzero.
113/// * The present value is nonzero and the future value is zero, unless the rate is exactly -1.0%.
114/// * The present value is negative and the future value is positive or vice versa.
115/// * The present value and future value are both negative, the future value is less than the
116/// present value, and the periodic rate is zero or negative.
117/// * The present value and future value are both negative, the future value is greater than the
118/// present value, and the periodic rate is zero or positive.
119/// * The present value and future value are both positive, the future value is greater than the
120/// present value, and the periodic rate is zero or negative.
121/// * The present value and future value are both positive, the future value is less than the
122/// present value, and the periodic rate is zero or positive.
123///
124/// # Examples
125/// ```
126/// use finance_solution::*;
127///
128/// // The interest rate is 8% per year.
129/// let rate = 0.08;
130///
131/// // The starting value is $5,000.00.
132/// let present_value = -5_000.00;
133///
134/// // The ending value is $7,000.00.
135/// let future_value = 7_000.00;
136///
137/// let continuous_compounding = false;
138///
139/// // Calculate the number of years required.
140/// let fractional_periods = periods(rate, present_value, future_value, false).unwrap();
141/// dbg!(&fractional_periods);
142/// assert_rounded_2(4.37, fractional_periods);
143///
144/// // Round up to get a whole number of years.
145/// let periods = fractional_periods.ceil() as u32;
146/// dbg!(&periods);
147/// assert_eq!(5, periods);
148/// ```
149/// # Errors
150/// Returns [`FinanceError`] when rate/values cannot produce a finite non-negative period count.
151pub fn periods<P, F, C>(
152    rate: f64,
153    present_value: P,
154    future_value: F,
155    compounding: C,
156) -> crate::FinanceResult<f64>
157where
158    P: Into<f64> + Copy,
159    F: Into<f64> + Copy,
160    C: Into<crate::Compounding>,
161{
162    periods_internal(
163        rate,
164        present_value.into(),
165        future_value.into(),
166        compounding.into().is_continuous(),
167    )
168}
169
170/// Calculates the number of periods given a periodic rate along with the present and future values
171/// using simple compounding; and builds a struct with the input values, an explanation of the
172/// formula, and the option to calculate the period-by-period values.
173///
174/// Note that the calculated number of periods from [PeriodsSolution::fractional_periods](./struct.PeriodsSolution.html#method.fractional_periods) field will
175/// be a floating point number. To get the periods as a whole number (rounded up) use
176/// [PeriodsSolution::periods](./struct.PeriodsSolution.html#method.periods).
177///
178/// See the [periods](./index.html) module page for the formulas.
179///
180/// Related functions:
181/// * To calculate the periods as a single number with simple compounding use [periods](fn.periods.html).
182/// * To calculate the periods using continuous compounding use [periods_continuous](fn.periods_continuous.html)
183/// or [periods_continuous_solution](fn.periods_continuous_solution.html).
184///
185/// # Arguments
186/// * `rate` - The rate at which the investment grows or shrinks per period, expressed as a
187/// floating point number. For instance 0.05 would mean 5% growth. Often appears as `r` or `i` in
188/// formulas.
189/// * `present_value` - The starting value of the investment. May appear as `pv` in formulas, or `P`
190/// for principal.
191/// * `future_value` - The final value of the investment.
192/// * `continuous_compounding` - True for continuous compounding, false for simple compounding.
193///
194/// # Errors
195/// The call returns [`FinanceError`] if the rate, the present value, or the future value is infinite or not a
196/// number (NaN).
197///
198/// The call will also fail in any of the follwing cases because there is no number of periods that
199/// would make the calculation work:
200/// * The periodic rate is less than -1.0.
201/// * The present value is zero and the future value is nonzero.
202/// * The present value is nonzero and the future value is zero, unless the rate is exactly -1.0%.
203/// * The present value is negative and the future value is positive or vice versa.
204/// * The present value and future value are both negative, the future value is less than the
205/// present value, and the periodic rate is zero or negative.
206/// * The present value and future value are both negative, the future value is greater than the
207/// present value, and the periodic rate is zero or positive.
208/// * The present value and future value are both positive, the future value is greater than the
209/// present value, and the periodic rate is zero or negative.
210/// * The present value and future value are both positive, the future value is less than the
211/// present value, and the periodic rate is zero or positive.
212///
213/// # Examples
214/// ```
215/// use finance_solution::*;
216///
217/// // The interest rate is 3.5% per quarter.
218/// let rate = 0.035;
219///
220/// // The starting value is $100,000.00.
221/// let present_value = -100_000.00;
222///
223/// // The ending value is $200,000.00.
224/// let future_value = 200_000.00;
225///
226/// // Use simple compounding.
227/// let continuous_compounding = false;
228///
229/// // Calculate the number of quarters required and build a struct with the
230/// // input values, an explanation of the formula, and an option to calculate
231/// // the quarter-by-quarter values.
232/// let solution = periods_solution(rate, present_value, future_value, continuous_compounding).unwrap();
233///
234/// let fractional_quarters = solution.fractional_periods();
235/// dbg!(&fractional_quarters);
236/// assert_rounded_2(20.15, fractional_quarters);
237///
238/// // Get the whole number of quarters.
239/// let quarters = solution.periods();
240/// dbg!(&quarters);
241/// assert_eq!(21, quarters);
242///
243/// // Examine the formulas.
244/// let formula = solution.formula();
245/// dbg!(&formula);
246/// assert_eq!("20.15 = log(-200000.0000 / -100000.0000, base 1.035000)", formula);
247/// let symbolic_formula = solution.symbolic_formula();
248/// dbg!(&symbolic_formula);
249/// assert_eq!("n = log(-fv / pv, base (1 + r))", symbolic_formula);
250///
251/// let series = solution.series();
252/// dbg!(&series);
253///
254/// let last_entry = series.last().unwrap();
255/// dbg!(&last_entry);
256/// assert_rounded_4(200_000.0, last_entry.value());
257///
258/// // Create a reduced series with the value at the end of each year.
259/// let filtered_series = series
260///     .iter()
261///     .filter(|x| x.period() % 4 == 0 && x.period() != 0)
262///     .collect::<Vec<_>>();
263/// dbg!(&filtered_series);
264/// assert_eq!(5, filtered_series.len());
265/// ```
266/// Negative interest rate.
267/// ```
268/// // The interest rate is -6% per year and the value falls from $15,000.00 to
269/// // $12,000.00.
270/// # use finance_solution::*;
271/// let solution = periods_solution(-0.06, -15_000.00, 12_000.00, false).unwrap();
272/// dbg!(&solution);
273/// assert_rounded_2(3.61, solution.fractional_periods());
274/// assert_eq!(4, solution.periods());
275///
276/// // Print the period-by-period values as a formatted table.
277/// solution.print_series_table();
278/// ```
279pub fn periods_solution<P, F, C>(
280    rate: f64,
281    present_value: P,
282    future_value: F,
283    compounding: C,
284) -> crate::FinanceResult<TvmSolution>
285where
286    P: Into<f64> + Copy,
287    F: Into<f64> + Copy,
288    C: Into<crate::Compounding>,
289{
290    periods_solution_internal(
291        rate,
292        present_value.into(),
293        future_value.into(),
294        compounding.into().is_continuous(),
295    )
296}
297
298pub(crate) fn periods_internal(
299    rate: f64,
300    present_value: f64,
301    future_value: f64,
302    continuous_compounding: bool,
303) -> crate::FinanceResult<f64> {
304    use crate::is_approx_equal;
305    crate::util::error::require_rate(rate)?;
306    crate::util::error::require_finite("present_value", present_value)?;
307    crate::util::error::require_finite("future_value", future_value)?;
308    if is_approx_equal!(0.0, present_value + future_value) {
309        return Ok(0.0);
310    }
311    if future_value == 0.0 && rate == -1.0 {
312        return Ok(1.0);
313    }
314    if present_value == 0.0 && future_value != 0.0 {
315        return Err(crate::FinanceError::Unsolvable {
316            message: "present value is zero and future value is nonzero; cannot solve for periods",
317        });
318    }
319    if present_value != 0.0 && future_value == 0.0 && rate != -1.0 {
320        return Err(crate::FinanceError::Unsolvable {
321            message: "future value is zero and rate is not -100%; cannot solve for periods",
322        });
323    }
324    if (present_value < 0.0 && future_value < 0.0) || (present_value > 0.0 && future_value > 0.0) {
325        return Err(crate::FinanceError::SameSignValues {
326            present_value,
327            future_value,
328        });
329    }
330    if present_value.abs() < future_value.abs() && rate <= 0.0 {
331        return Err(crate::FinanceError::Unsolvable {
332            message: "with non-positive rate, |pv| < |fv| cannot be reached",
333        });
334    }
335    if present_value.abs() > future_value.abs() && rate >= 0.0 {
336        return Err(crate::FinanceError::Unsolvable {
337            message: "with non-negative rate, |pv| > |fv| cannot be reached",
338        });
339    }
340    let fractional_periods = if continuous_compounding {
341        (-future_value / present_value).ln() / rate
342    } else {
343        (-future_value / present_value).log(1.0 + rate)
344    };
345    if fractional_periods.is_finite() && fractional_periods >= 0.0 {
346        Ok(fractional_periods)
347    } else {
348        Err(crate::FinanceError::NonFinite {
349            field: "periods",
350            value: fractional_periods,
351        })
352    }
353}
354
355pub(crate) fn periods_solution_internal(
356    rate: f64,
357    present_value: f64,
358    future_value: f64,
359    continuous_compounding: bool,
360) -> crate::FinanceResult<TvmSolution> {
361    let fractional_periods =
362        periods_internal(rate, present_value, future_value, continuous_compounding)?;
363    let (formula, symbolic_formula) = if continuous_compounding {
364        let formula = format!(
365            "{:.2} = ln({:.4} / {:.4}) / {:.6}",
366            fractional_periods, -future_value, present_value, rate
367        );
368        let symbolic_formula = "n = ln(-fv / pv) / r";
369        (formula, symbolic_formula)
370    } else {
371        let rate_multiplier = 1.0 + rate;
372        let formula = format!(
373            "{:.2} = log({:.4} / {:.4}, base {:.6})",
374            fractional_periods, -future_value, present_value, rate_multiplier
375        );
376        let symbolic_formula = "n = log(-fv / pv, base (1 + r))";
377        (formula, symbolic_formula)
378    };
379    Ok(TvmSolution::new_fractional_periods(
380        TvmVariable::Periods,
381        continuous_compounding,
382        rate,
383        fractional_periods,
384        present_value,
385        future_value,
386        &formula,
387        symbolic_formula,
388    ))
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use crate::*;
395
396    #[test]
397    fn test_periods_edge() {
398        // Present and future values add up to zero so no periods are needed.
399        assert_rounded_2(0.0, periods(0.04, 10_000.0, -10_000.0, false).unwrap());
400
401        // The present value is negative and the future value is zero, which works only if the rate
402        // is exactly -1.0%.
403        assert_rounded_6(1.0, periods(-1.0, -10_000.0, 0.0, false).unwrap());
404
405        // The present value is positive and the future value is zero, which works only if the rate
406        // is exactly -1.0%.
407        assert_rounded_6(1.0, periods(-1.0, 10_000.0, 0.0, false).unwrap());
408    }
409
410    #[test]
411    fn test_periods_err_rate_nan() {
412        assert!(periods(std::f64::NAN, 1_000.0, 2_000.0, false).is_err());
413    }
414
415    #[test]
416    fn test_periods_err_rate_inf() {
417        assert!(periods(std::f64::NEG_INFINITY, 1_000.0, 2_000.0, false).is_err());
418    }
419
420    #[test]
421    fn test_periods_err_present_value_nan() {
422        assert!(periods(0.04, std::f64::NAN, 1_000.0, false).is_err());
423    }
424
425    #[test]
426    fn test_periods_err_present_value_inf() {
427        assert!(periods(0.04, std::f64::INFINITY, 1_000.0, false).is_err());
428    }
429
430    #[test]
431    fn test_periods_err_future_value_nan() {
432        assert!(periods(0.04, 1_000.0, std::f64::NAN, false).is_err());
433    }
434
435    #[test]
436    fn test_periods_err_future_value_inf() {
437        assert!(periods(0.04, 1_000.0, std::f64::NEG_INFINITY, false).is_err());
438    }
439
440    #[test]
441    fn test_periods_err_future_greater_bad_rate_1() {
442        // The future value is greater than the present value and the periodic rate is zero.
443        assert!(periods(0.0, 1_000.0, 2_000.0, false).is_err());
444    }
445
446    #[test]
447    fn test_periods_err_future_greater_bad_rate_2() {
448        // The future value is greater than the present value and the periodic rate is negative.
449        assert!(periods(-0.04, 1_000.0, 2_000.0, false).is_err());
450    }
451
452    #[test]
453    fn test_periods_err_future_less_bad_rate_1() {
454        // The future value is less than the present value and the periodic rate is zero.
455        assert!(periods(0.0, 2_000.0, 1_000.0, false).is_err());
456    }
457
458    #[test]
459    fn test_periods_err_future_less_bad_rate_2() {
460        // The future value is less than the present value and the periodic rate is positive.
461        assert!(periods(0.04, 2_000.0, 1_000.0, false).is_err());
462    }
463
464    #[test]
465    fn test_periods_err_present_zero_future_negative() {
466        // The present value is zero and the future value is negative.
467        assert!(periods(0.04, 0.0, -1_000.0, false).is_err());
468    }
469
470    #[test]
471    fn test_periods_err_present_zero_future_positive() {
472        // The present value is zero and the future value is positive.
473        assert!(periods(0.04, 0.0, 1_000.0, false).is_err());
474    }
475
476    #[test]
477    fn test_periods_err_present_negative_future_zero() {
478        // The present value is negative and the future value is zero.
479        assert!(periods(0.04, -1_000.0, 0.0, false).is_err());
480    }
481
482    #[test]
483    fn test_periods_err_present_positive_future_zero() {
484        // The present value is positive and the future value is zero. This will fail unless the
485        // rate is exactly -1.0%.
486        assert!(periods(-0.04, 1_000.0, 0.0, false).is_err());
487    }
488
489    #[test]
490    fn test_periods_err_present_negative_future_negative() {
491        // The present value and future value are both negative.
492        assert!(periods(0.04, -1_000.0, -1_000.0, false).is_err());
493    }
494
495    #[test]
496    fn test_periods_err_present_positive_future_positive() {
497        // The present value and future value are buth positive.
498        assert!(periods(0.04, 1_000.0, 1_000.0, false).is_err());
499    }
500
501    /*
502    macro_rules! compare_to_excel {
503        ( $r:expr, $pv:expr, $fv:expr, $n_excel:expr, $n_manual_simple:expr, $n_manual_cont:expr ) => {
504            println!("$r = {}, $pv = {}, $fv = {}, $n_excel: {}, $n_manual_simple = {}, $n_manual_cont = {}", $r, $pv, $fv, $n_excel, $n_manual_simple, $n_manual_cont);
505            assert_approx_equal!($n_excel, $n_manual_simple);
506
507            let n_calc_simple = periods($r, $pv, $fv, false).unwrap();
508            println!("n_calc_simple = {}", n_calc_simple);
509            assert_approx_equal!($n_excel, n_calc_simple);
510
511            let n_calc_cont = periods($r, $pv, $fv, true).unwrap();
512            println!("n_calc_cont = {}", n_calc_cont);
513            assert_approx_equal!($n_manual_cont, n_calc_cont);
514
515            if is_approx_equal!(0.0, n_calc_simple) {
516                assert_approx_equal!(0.0, n_calc_cont);
517            } else {
518                let ratio = n_calc_cont / n_calc_simple;
519                println!("ratio = {}", ratio);
520                if $r < 0.0 {
521                    assert!(ratio >= 1.0);
522                    assert!(ratio <= 2.0);
523                } else {
524                    assert!(ratio >= 0.0);
525                    assert!(ratio <= 1.0);
526                }
527            }
528        }
529    }
530    */
531
532    fn compare_to_excel(
533        test_case: usize,
534        r: f64,
535        pv: f64,
536        fv: f64,
537        n_excel: f64,
538        n_manual_simple: f64,
539        n_manual_cont: f64,
540    ) {
541        let display = false;
542
543        if display {
544            println!("test_case = {}, r = {}, pv = {}, fv = {}, n_excel: {}, n_manual_simple = {}, n_manual_cont = {}", test_case, r, pv, fv, n_excel, n_manual_simple, n_manual_cont)
545        };
546        assert_approx_equal!(n_excel, n_manual_simple);
547
548        let n_calc_simple = periods(r, pv, fv, false).unwrap();
549        if display {
550            println!("n_calc_simple = {}", n_calc_simple)
551        };
552        assert_approx_equal!(n_excel, n_calc_simple);
553
554        let n_calc_cont = periods(r, pv, fv, true).unwrap();
555        if display {
556            println!("n_calc_cont = {}", n_calc_cont)
557        };
558        assert_approx_equal!(n_manual_cont, n_calc_cont);
559
560        if is_approx_equal!(0.0, n_calc_simple) {
561            assert_approx_equal!(0.0, n_calc_cont);
562        } else {
563            let ratio = n_calc_cont / n_calc_simple;
564            if display {
565                println!("ratio = {}", ratio)
566            };
567            if r < 0.0 {
568                assert!(ratio >= 1.0);
569                assert!(ratio <= 2.0);
570            } else {
571                assert!(ratio >= 0.0);
572                assert!(ratio <= 1.0);
573            }
574        }
575
576        // Solution with simple compounding.
577        let solution = periods_solution(r, pv, fv, false).unwrap();
578        if display {
579            dbg!(&solution);
580        }
581        solution.invariant();
582        assert!(solution.calculated_field().is_periods());
583        assert_eq!(false, solution.continuous_compounding());
584        assert_approx_equal!(r, solution.rate());
585        assert_approx_equal!(n_excel, solution.fractional_periods());
586        assert_approx_equal!(pv, solution.present_value());
587        assert_approx_equal!(fv, solution.future_value());
588
589        // Solution with continuous compounding.
590        let solution = periods_solution(r, pv, fv, true).unwrap();
591        if display {
592            dbg!(&solution);
593        }
594        solution.invariant();
595        assert!(solution.calculated_field().is_periods());
596        assert!(solution.continuous_compounding());
597        assert_approx_equal!(r, solution.rate());
598        assert_approx_equal!(n_manual_cont as f64, solution.fractional_periods());
599        assert_approx_equal!(pv, solution.present_value());
600        assert_approx_equal!(fv, solution.future_value());
601    }
602
603    #[test]
604    fn test_periods_against_excel() {
605        compare_to_excel(
606            1,
607            0.01f64,
608            -0.1f64,
609            1f64,
610            231.407892558761f64,
611            231.407892558761f64,
612            230.258509299405f64,
613        );
614        compare_to_excel(
615            2,
616            0.07f64,
617            1.05f64,
618            -1.5f64,
619            5.27168295531017f64,
620            5.27168295531017f64,
621            5.09535634198189f64,
622        );
623        compare_to_excel(3, 0.05f64, -2.25f64, 2.25f64, 0f64, 0f64, 0f64);
624        compare_to_excel(
625            4,
626            -0.01f64,
627            4.3875f64,
628            -3.375f64,
629            26.1050245774708f64,
630            26.1050245774708f64,
631            26.2364264467491f64,
632        );
633        compare_to_excel(
634            5,
635            -0.07f64,
636            -10.125f64,
637            5.0625f64,
638            9.55133750944734f64,
639            9.55133750944734f64,
640            9.90210257942779f64,
641        );
642        compare_to_excel(
643            6,
644            0.011f64,
645            0.759375f64,
646            -7.59375f64,
647            210.475110917029f64,
648            210.475110917029f64,
649            209.325917544913f64,
650        );
651        compare_to_excel(
652            7,
653            0.077f64,
654            -7.9734375f64,
655            11.390625f64,
656            4.80827497549769f64,
657            4.80827497549769f64,
658            4.63214212907445f64,
659        );
660        compare_to_excel(8, 0.055f64, 17.0859375f64, -17.0859375f64, 0f64, 0f64, 0f64);
661        compare_to_excel(
662            9,
663            -0.011f64,
664            -33.317578125f64,
665            25.62890625f64,
666            23.7198728049214f64,
667            23.7198728049214f64,
668            23.8512967697719f64,
669        );
670        compare_to_excel(
671            10,
672            -0.077f64,
673            76.88671875f64,
674            -38.443359375f64,
675            8.65071007890757f64,
676            8.65071007890757f64,
677            9.00191143584344f64,
678        );
679        compare_to_excel(
680            11,
681            0.0121f64,
682            -5.76650390625f64,
683            57.6650390625f64,
684            191.44527339039f64,
685            191.44527339039f64,
686            190.296288677194f64,
687        );
688        compare_to_excel(
689            12,
690            0.0847f64,
691            60.548291015625f64,
692            -86.49755859375f64,
693            4.38695946200137f64,
694            4.38695946200137f64,
695            4.21103829915859f64,
696        );
697        compare_to_excel(
698            13,
699            0.0605f64,
700            -129.746337890625f64,
701            129.746337890625f64,
702            0f64,
703            0f64,
704            0f64,
705        );
706        compare_to_excel(
707            14,
708            -0.0121f64,
709            253.005358886719f64,
710            -194.619506835937f64,
711            21.5515487676588f64,
712            21.5515487676588f64,
713            21.682997063429f64,
714        );
715        compare_to_excel(
716            15,
717            -0.0847f64,
718            -583.858520507812f64,
719            291.929260253906f64,
720            7.83187077097481f64,
721            7.83187077097481f64,
722            8.18355585076677f64,
723        );
724        compare_to_excel(
725            16,
726            0.01331f64,
727            43.7893890380859f64,
728            -437.893890380859f64,
729            174.145381520649f64,
730            174.145381520649f64,
731            172.996626070176f64,
732        );
733        compare_to_excel(
734            17,
735            0.09317f64,
736            -459.788584899902f64,
737            656.840835571289f64,
738            4.00390668920358f64,
739            4.00390668920358f64,
740            3.82821663559871f64,
741        );
742        compare_to_excel(
743            18,
744            0.06655f64,
745            985.261253356933f64,
746            -985.261253356933f64,
747            0f64,
748            0f64,
749            0f64,
750        );
751        compare_to_excel(
752            19,
753            -0.01331f64,
754            -1921.25944404602f64,
755            1477.8918800354f64,
756            19.5803404211536f64,
757            19.5803404211536f64,
758            19.7118155122082f64,
759        );
760        compare_to_excel(
761            20,
762            -0.09317f64,
763            4433.6756401062f64,
764            -2216.8378200531f64,
765            7.08737437249388f64,
766            7.08737437249388f64,
767            7.43959622796979f64,
768        );
769        compare_to_excel(
770            21,
771            0.014641f64,
772            -332.525673007965f64,
773            3325.25673007965f64,
774            158.418163641677f64,
775            158.418163641677f64,
776            157.269660063797f64,
777        );
778        compare_to_excel(
779            22,
780            0.102487f64,
781            3491.51956658363f64,
782            -4987.88509511947f64,
783            3.65563485240142f64,
784            3.65563485240142f64,
785            3.48019694145338f64,
786        );
787        compare_to_excel(
788            23,
789            0.073205f64,
790            -7481.82764267921f64,
791            7481.82764267921f64,
792            0f64,
793            0f64,
794            0f64,
795        );
796        compare_to_excel(
797            24,
798            -0.014641f64,
799            14589.5639032245f64,
800            -11222.7414640188f64,
801            17.7883276800301f64,
802            17.7883276800301f64,
803            17.9198322838256f64,
804        );
805    }
806}