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}