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/// # Panics
106/// The call will fail 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);
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/// ```
149pub fn periods<P, F>(rate: f64, present_value: P, future_value: F, continuous_compounding: bool) -> f64
150    where
151        P: Into<f64> + Copy,
152        F: Into<f64> + Copy
153{
154    periods_internal(rate, present_value.into(), future_value.into(), continuous_compounding)
155}
156
157/// Calculates the number of periods given a periodic rate along with the present and future values
158/// using simple compounding; and builds a struct with the input values, an explanation of the
159/// formula, and the option to calculate the period-by-period values.
160///
161/// Note that the calculated number of periods from [PeriodsSolution::fractional_periods](./struct.PeriodsSolution.html#method.fractional_periods) field will
162/// be a floating point number. To get the periods as a whole number (rounded up) use
163/// [PeriodsSolution::periods](./struct.PeriodsSolution.html#method.periods).
164///
165/// See the [periods](./index.html) module page for the formulas.
166///
167/// Related functions:
168/// * To calculate the periods as a single number with simple compounding use [periods](fn.periods.html).
169/// * To calculate the periods using continuous compounding use [periods_continuous](fn.periods_continuous.html)
170/// or [periods_continuous_solution](fn.periods_continuous_solution.html).
171///
172/// # Arguments
173/// * `rate` - The rate at which the investment grows or shrinks per period, expressed as a
174/// floating point number. For instance 0.05 would mean 5% growth. Often appears as `r` or `i` in
175/// formulas.
176/// * `present_value` - The starting value of the investment. May appear as `pv` in formulas, or `P`
177/// for principal.
178/// * `future_value` - The final value of the investment.
179/// * `continuous_compounding` - True for continuous compounding, false for simple compounding.
180///
181/// # Panics
182/// The call will fail if the rate, the present value, or the future value is infinite or not a
183/// number (NaN).
184///
185/// The call will also fail in any of the follwing cases because there is no number of periods that
186/// would make the calculation work:
187/// * The periodic rate is less than -1.0.
188/// * The present value is zero and the future value is nonzero.
189/// * The present value is nonzero and the future value is zero, unless the rate is exactly -1.0%.
190/// * The present value is negative and the future value is positive or vice versa.
191/// * The present value and future value are both negative, the future value is less than the
192/// present value, and the periodic rate is zero or negative.
193/// * The present value and future value are both negative, the future value is greater than the
194/// present value, and the periodic rate is zero or positive.
195/// * The present value and future value are both positive, the future value is greater than the
196/// present value, and the periodic rate is zero or negative.
197/// * The present value and future value are both positive, the future value is less than the
198/// present value, and the periodic rate is zero or positive.
199///
200/// # Examples
201/// ```
202/// use finance_solution::*;
203///
204/// // The interest rate is 3.5% per quarter.
205/// let rate = 0.035;
206///
207/// // The starting value is $100,000.00.
208/// let present_value = -100_000.00;
209///
210/// // The ending value is $200,000.00.
211/// let future_value = 200_000.00;
212///
213/// // Use simple compounding.
214/// let continuous_compounding = false;
215///
216/// // Calculate the number of quarters required and build a struct with the
217/// // input values, an explanation of the formula, and an option to calculate
218/// // the quarter-by-quarter values.
219/// let solution = periods_solution(rate, present_value, future_value, continuous_compounding);
220///
221/// let fractional_quarters = solution.fractional_periods();
222/// dbg!(&fractional_quarters);
223/// assert_rounded_2(20.15, fractional_quarters);
224///
225/// // Get the whole number of quarters.
226/// let quarters = solution.periods();
227/// dbg!(&quarters);
228/// assert_eq!(21, quarters);
229///
230/// // Examine the formulas.
231/// let formula = solution.formula();
232/// dbg!(&formula);
233/// assert_eq!("20.15 = log(-200000.0000 / -100000.0000, base 1.035000)", formula);
234/// let symbolic_formula = solution.symbolic_formula();
235/// dbg!(&symbolic_formula);
236/// assert_eq!("n = log(-fv / pv, base (1 + r))", symbolic_formula);
237///
238/// let series = solution.series();
239/// dbg!(&series);
240///
241/// let last_entry = series.last().unwrap();
242/// dbg!(&last_entry);
243/// assert_rounded_4(200_000.0, last_entry.value());
244///
245/// // Create a reduced series with the value at the end of each year.
246/// let filtered_series = series
247///     .iter()
248///     .filter(|x| x.period() % 4 == 0 && x.period() != 0)
249///     .collect::<Vec<_>>();
250/// dbg!(&filtered_series);
251/// assert_eq!(5, filtered_series.len());
252/// ```
253/// Negative interest rate.
254/// ```
255/// // The interest rate is -6% per year and the value falls from $15,000.00 to
256/// // $12,000.00.
257/// # use finance_solution::*;
258/// let solution = periods_solution(-0.06, -15_000.00, 12_000.00, false);
259/// dbg!(&solution);
260/// assert_rounded_2(3.61, solution.fractional_periods());
261/// assert_eq!(4, solution.periods());
262///
263/// // Print the period-by-period values as a formatted table.
264/// solution.print_series_table();
265/// ```
266pub fn periods_solution<P, F>(rate: f64, present_value: P, future_value: F, continuous_compounding: bool) -> TvmSolution
267    where
268        P: Into<f64> + Copy,
269        F: Into<f64> + Copy
270{
271    periods_solution_internal(rate, present_value.into(), future_value.into(), continuous_compounding)
272}
273
274pub(crate) fn periods_internal(rate: f64, present_value: f64, future_value: f64, continuous_compounding: bool) -> f64 {
275    if is_approx_equal!(0.0, present_value + future_value) {
276        // This is a special case that doesn't require us to check the parameters and which covers
277        // the case where both are zero.
278        return 0.0;
279    }
280    if future_value == 0.0 && rate == -1.0 {
281        // This is a special case that we can't run through the log function. Since the rate is
282        // -100%, given any present value the future value will be zero and it will take only one
283        // period to get there.
284        // We already know that the present value is nonzero because that case would have been
285        // caught above.
286        assert!(present_value != 0.0);
287        return 1.0;
288    }
289
290    check_periods_parameters(rate, present_value, future_value);
291
292    let fractional_periods = if continuous_compounding {
293        // http://www.edmichaelreggie.com/TMVContent/rate.htm
294        (-future_value / present_value).ln() / rate
295    } else {
296        (-future_value / present_value).log(1.0 + rate)
297    };
298    assert!(fractional_periods >= 0.0);
299    fractional_periods
300}
301
302pub(crate) fn periods_solution_internal(rate: f64, present_value: f64, future_value: f64, continuous_compounding: bool) -> TvmSolution {
303    let fractional_periods = periods_internal(rate, present_value, future_value, continuous_compounding);
304    assert!(fractional_periods >= 0.0);
305    let (formula, symbolic_formula) = if continuous_compounding {
306        let formula = format!("{:.2} = ln({:.4} / {:.4}) / {:.6}", fractional_periods, -future_value, present_value, rate);
307        let symbolic_formula = "n = ln(-fv / pv) / r";
308        (formula, symbolic_formula)
309    } else {
310        let rate_multiplier = 1.0 + rate;
311        let formula = format!("{:.2} = log({:.4} / {:.4}, base {:.6})", fractional_periods, -future_value, present_value, rate_multiplier);
312        let symbolic_formula = "n = log(-fv / pv, base (1 + r))";
313        (formula, symbolic_formula)
314    };
315    TvmSolution::new_fractional_periods(TvmVariable::Periods,continuous_compounding, rate, fractional_periods, present_value, future_value, &formula, symbolic_formula)
316}
317
318fn check_periods_parameters(rate: f64, present_value: f64, future_value: f64) {
319    assert!(rate.is_finite(), "The rate must be finite (not NaN or infinity)");
320    assert!(present_value.is_finite(), "The present value must be finite (not NaN or infinity)");
321    assert!(future_value.is_finite(), "The future value must be finite (not NaN or infinity)");
322    assert!(rate >= -1.0, "The rate must be greater than or equal to -1.0 because a rate lower than -100% would mean the investment loses more than its full value in a period.");
323    assert!(!(present_value == 0.0 && future_value != 0.0), "The present value is zero and the future value is nonzero so there's no way to solve for the number of periods.");
324    assert!(!(present_value != 0.0 && future_value == 0.0 && rate != -1.0), "The present value is nonzero, the future value is zero, and the rate is not -100% so there's no way to solve for the number of periods.");
325    assert!(!(present_value < 0.0 && future_value < 0.0), "The present value and future value are both negative. They must have opposite signs.");
326    assert!(!(present_value > 0.0 && future_value > 0.0), "The present value and future value are both positive. They must have opposite signs.");
327    assert!(!(present_value.abs() < future_value.abs() && rate <= 0.0), "The absolute value of the present value is less than the absolute value of the future value and the periodic rate is zero or negative. There's no way to solve for the number of periods because no amount of compounding will reach the future value.");
328    assert!(!(present_value.abs() > future_value.abs() && rate >= 0.0), "The absolute value of the present value is greater than the absolute value of the future value and the periodic rate is zero or positive. There's no way to solve for the number of periods because no amount of compounding will reach the future value.");
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use crate::*;
335
336    #[test]
337    fn test_periods_edge() {
338        // Present and future values add up to zero so no periods are needed.
339        assert_rounded_2(0.0, periods(0.04, 10_000.0, -10_000.0, false));
340
341        // The present value is negative and the future value is zero, which works only if the rate
342        // is exactly -1.0%.
343        assert_rounded_6(1.0, periods(-1.0, -10_000.0, 0.0, false));
344
345        // The present value is positive and the future value is zero, which works only if the rate
346        // is exactly -1.0%.
347        assert_rounded_6(1.0, periods(-1.0, 10_000.0, 0.0, false));
348    }
349
350    #[should_panic]
351    #[test]
352    fn test_periods_err_rate_nan() {
353        periods(std::f64::NAN, 1_000.0, 2_000.0, false);
354    }
355
356    #[should_panic]
357    #[test]
358    fn test_periods_err_rate_inf() {
359        periods(std::f64::NEG_INFINITY, 1_000.0, 2_000.0, false);
360    }
361
362    #[should_panic]
363    #[test]
364    fn test_periods_err_present_value_nan() {
365        periods(0.04, std::f64::NAN, 1_000.0, false);
366    }
367
368    #[should_panic]
369    #[test]
370    fn test_periods_err_present_value_inf() {
371        periods(0.04, std::f64::INFINITY, 1_000.0, false);
372    }
373
374    #[should_panic]
375    #[test]
376    fn test_periods_err_future_value_nan() {
377        periods(0.04, 1_000.0, std::f64::NAN, false);
378    }
379
380    #[should_panic]
381    #[test]
382    fn test_periods_err_future_value_inf() {
383        periods(0.04, 1_000.0, std::f64::NEG_INFINITY, false);
384    }
385
386    #[should_panic]
387    #[test]
388    fn test_periods_err_future_greater_bad_rate_1() {
389        // The future value is greater than the present value and the periodic rate is zero.
390        periods(0.0, 1_000.0, 2_000.0, false);
391    }
392
393    #[should_panic]
394    #[test]
395    fn test_periods_err_future_greater_bad_rate_2() {
396        // The future value is greater than the present value and the periodic rate is negative.
397        periods(-0.04, 1_000.0, 2_000.0, false);
398    }
399
400    #[should_panic]
401    #[test]
402    fn test_periods_err_future_less_bad_rate_1() {
403        // The future value is less than the present value and the periodic rate is zero.
404        periods(0.0, 2_000.0, 1_000.0, false);
405    }
406
407    #[should_panic]
408    #[test]
409    fn test_periods_err_future_less_bad_rate_2() {
410        // The future value is less than the present value and the periodic rate is positive.
411        periods(0.04, 2_000.0, 1_000.0, false);
412    }
413
414    #[should_panic]
415    #[test]
416    fn test_periods_err_present_zero_future_negative() {
417        // The present value is zero and the future value is negative.
418        periods(0.04, 0.0, -1_000.0, false);
419    }
420
421    #[should_panic]
422    #[test]
423    fn test_periods_err_present_zero_future_positive() {
424        // The present value is zero and the future value is positive.
425        periods(0.04, 0.0, 1_000.0, false);
426    }
427
428    #[should_panic]
429    #[test]
430    fn test_periods_err_present_negative_future_zero() {
431        // The present value is negative and the future value is zero.
432        periods(0.04, -1_000.0, 0.0, false);
433    }
434
435    #[should_panic]
436    #[test]
437    fn test_periods_err_present_positive_future_zero() {
438        // The present value is positive and the future value is zero. This will fail unless the
439        // rate is exactly -1.0%.
440        periods(-0.04, 1_000.0, 0.0, false);
441    }
442
443    #[should_panic]
444    #[test]
445    fn test_periods_err_present_negative_future_negative() {
446        // The present value and future value are both negative.
447        periods(0.04, -1_000.0, -1_000.0, false);
448    }
449
450    #[should_panic]
451    #[test]
452    fn test_periods_err_present_positive_future_positive() {
453        // The present value and future value are buth positive.
454        periods(0.04, 1_000.0, 1_000.0, false);
455    }
456
457    /*
458    macro_rules! compare_to_excel {
459        ( $r:expr, $pv:expr, $fv:expr, $n_excel:expr, $n_manual_simple:expr, $n_manual_cont:expr ) => {
460            println!("$r = {}, $pv = {}, $fv = {}, $n_excel: {}, $n_manual_simple = {}, $n_manual_cont = {}", $r, $pv, $fv, $n_excel, $n_manual_simple, $n_manual_cont);
461            assert_approx_equal!($n_excel, $n_manual_simple);
462
463            let n_calc_simple = periods($r, $pv, $fv, false);
464            println!("n_calc_simple = {}", n_calc_simple);
465            assert_approx_equal!($n_excel, n_calc_simple);
466
467            let n_calc_cont = periods($r, $pv, $fv, true);
468            println!("n_calc_cont = {}", n_calc_cont);
469            assert_approx_equal!($n_manual_cont, n_calc_cont);
470
471            if is_approx_equal!(0.0, n_calc_simple) {
472                assert_approx_equal!(0.0, n_calc_cont);
473            } else {
474                let ratio = n_calc_cont / n_calc_simple;
475                println!("ratio = {}", ratio);
476                if $r < 0.0 {
477                    assert!(ratio >= 1.0);
478                    assert!(ratio <= 2.0);
479                } else {
480                    assert!(ratio >= 0.0);
481                    assert!(ratio <= 1.0);
482                }
483            }
484        }
485    }
486    */
487
488    fn compare_to_excel (test_case: usize, r: f64, pv: f64, fv: f64, n_excel: f64, n_manual_simple: f64, n_manual_cont: f64) {
489        let display = false;
490
491        if display { 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) };
492        assert_approx_equal!(n_excel, n_manual_simple);
493
494        let n_calc_simple = periods(r, pv, fv,false);
495        if display { println!("n_calc_simple = {}", n_calc_simple) };
496        assert_approx_equal!(n_excel, n_calc_simple);
497
498        let n_calc_cont = periods(r, pv, fv, true);
499        if display { println!("n_calc_cont = {}", n_calc_cont) };
500        assert_approx_equal!(n_manual_cont, n_calc_cont);
501
502        if is_approx_equal!(0.0, n_calc_simple) {
503            assert_approx_equal!(0.0, n_calc_cont);
504        } else {
505            let ratio = n_calc_cont / n_calc_simple;
506            if display { println!("ratio = {}", ratio) };
507            if r < 0.0 {
508                assert!(ratio >= 1.0);
509                assert!(ratio <= 2.0);
510            } else {
511                assert!(ratio >= 0.0);
512                assert!(ratio <= 1.0);
513            }
514        }
515
516        // Solution with simple compounding.
517        let solution = periods_solution(r, pv, fv, false);
518        if display { dbg!(&solution); }
519        solution.invariant();
520        assert!(solution.calculated_field().is_periods());
521        assert_eq!(false, solution.continuous_compounding());
522        assert_approx_equal!(r, solution.rate());
523        assert_approx_equal!(n_excel, solution.fractional_periods());
524        assert_approx_equal!(pv, solution.present_value());
525        assert_approx_equal!(fv, solution.future_value());
526
527        // Solution with continuous compounding.
528        let solution = periods_solution(r, pv, fv, true);
529        if display { dbg!(&solution); }
530        solution.invariant();
531        assert!(solution.calculated_field().is_periods());
532        assert!(solution.continuous_compounding());
533        assert_approx_equal!(r, solution.rate());
534        assert_approx_equal!(n_manual_cont as f64, solution.fractional_periods());
535        assert_approx_equal!(pv, solution.present_value());
536        assert_approx_equal!(fv, solution.future_value());
537    }
538    
539    #[test]
540    fn test_periods_against_excel() {
541        compare_to_excel(1, 0.01f64, -0.1f64, 1f64, 231.407892558761f64, 231.407892558761f64, 230.258509299405f64);
542        compare_to_excel(2, 0.07f64, 1.05f64, -1.5f64, 5.27168295531017f64, 5.27168295531017f64, 5.09535634198189f64);
543        compare_to_excel(3, 0.05f64, -2.25f64, 2.25f64, 0f64, 0f64, 0f64);
544        compare_to_excel(4, -0.01f64, 4.3875f64, -3.375f64, 26.1050245774708f64, 26.1050245774708f64, 26.2364264467491f64);
545        compare_to_excel(5, -0.07f64, -10.125f64, 5.0625f64, 9.55133750944734f64, 9.55133750944734f64, 9.90210257942779f64);
546        compare_to_excel(6, 0.011f64, 0.759375f64, -7.59375f64, 210.475110917029f64, 210.475110917029f64, 209.325917544913f64);
547        compare_to_excel(7, 0.077f64, -7.9734375f64, 11.390625f64, 4.80827497549769f64, 4.80827497549769f64, 4.63214212907445f64);
548        compare_to_excel(8, 0.055f64, 17.0859375f64, -17.0859375f64, 0f64, 0f64, 0f64);
549        compare_to_excel(9, -0.011f64, -33.317578125f64, 25.62890625f64, 23.7198728049214f64, 23.7198728049214f64, 23.8512967697719f64);
550        compare_to_excel(10, -0.077f64, 76.88671875f64, -38.443359375f64, 8.65071007890757f64, 8.65071007890757f64, 9.00191143584344f64);
551        compare_to_excel(11, 0.0121f64, -5.76650390625f64, 57.6650390625f64, 191.44527339039f64, 191.44527339039f64, 190.296288677194f64);
552        compare_to_excel(12, 0.0847f64, 60.548291015625f64, -86.49755859375f64, 4.38695946200137f64, 4.38695946200137f64, 4.21103829915859f64);
553        compare_to_excel(13, 0.0605f64, -129.746337890625f64, 129.746337890625f64, 0f64, 0f64, 0f64);
554        compare_to_excel(14, -0.0121f64, 253.005358886719f64, -194.619506835937f64, 21.5515487676588f64, 21.5515487676588f64, 21.682997063429f64);
555        compare_to_excel(15, -0.0847f64, -583.858520507812f64, 291.929260253906f64, 7.83187077097481f64, 7.83187077097481f64, 8.18355585076677f64);
556        compare_to_excel(16, 0.01331f64, 43.7893890380859f64, -437.893890380859f64, 174.145381520649f64, 174.145381520649f64, 172.996626070176f64);
557        compare_to_excel(17, 0.09317f64, -459.788584899902f64, 656.840835571289f64, 4.00390668920358f64, 4.00390668920358f64, 3.82821663559871f64);
558        compare_to_excel(18, 0.06655f64, 985.261253356933f64, -985.261253356933f64, 0f64, 0f64, 0f64);
559        compare_to_excel(19, -0.01331f64, -1921.25944404602f64, 1477.8918800354f64, 19.5803404211536f64, 19.5803404211536f64, 19.7118155122082f64);
560        compare_to_excel(20, -0.09317f64, 4433.6756401062f64, -2216.8378200531f64, 7.08737437249388f64, 7.08737437249388f64, 7.43959622796979f64);
561        compare_to_excel(21, 0.014641f64, -332.525673007965f64, 3325.25673007965f64, 158.418163641677f64, 158.418163641677f64, 157.269660063797f64);
562        compare_to_excel(22, 0.102487f64, 3491.51956658363f64, -4987.88509511947f64, 3.65563485240142f64, 3.65563485240142f64, 3.48019694145338f64);
563        compare_to_excel(23, 0.073205f64, -7481.82764267921f64, 7481.82764267921f64, 0f64, 0f64, 0f64);
564        compare_to_excel(24, -0.014641f64, 14589.5639032245f64, -11222.7414640188f64, 17.7883276800301f64, 17.7883276800301f64, 17.9198322838256f64);
565    }
566}