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}