finance_solution/tvm/
mod.rs

1//! The internal module which supports the solution struct for the family of Time-value-of-money equations
2//! which do not involve payments. For example, future value, present value, rate, and periods.
3
4use crate::*;
5use std::ops::Deref;
6use std::fmt::{Display, Formatter, Error};
7
8pub mod future_value;
9#[doc(inline)]
10pub use future_value::*;
11
12pub mod present_value;
13#[doc(inline)]
14pub use present_value::*;
15
16pub mod periods;
17#[doc(inline)]
18pub use periods::*;
19
20pub mod rate;
21#[doc(inline)]
22pub use rate::*;
23
24/// Enumeration used for the `calculated_field` field in [`TvmSolution`] and [`TvmSchedule`] to keep
25/// track of what was calculated, either the periodic rate, the number of periods, the present
26/// value, or the future value.
27#[derive(Clone, Debug, Hash, PartialEq)]
28pub enum TvmVariable {
29    Rate,
30    Periods,
31    PresentValue,
32    FutureValue,
33}
34
35#[derive(Clone, Debug)]
36pub struct TvmSolution {
37    calculated_field: TvmVariable,
38    continuous_compounding: bool,
39    rate: f64,
40    periods: u32,
41    fractional_periods: f64,
42    present_value: f64,
43    future_value: f64,
44    formula: String,
45    symbolic_formula: String,
46}
47
48/// A record of a Time Value of Money calculation where the rate may vary by period.
49///
50/// It's the result of calling [FutureValueScheduleSolution.tvm_solution](./struct.FutureValueScheduleSolution.html#method.tvm_solution)
51/// or [PresentValueScheduleSolution.tvm_solution](./struct.PresentValueScheduleSolution.html#method.tvm_solution)
52#[derive(Clone, Debug)]
53pub struct TvmScheduleSolution {
54    calculated_field: TvmVariable,
55    rates: Vec<f64>,
56    periods: u32,
57    present_value: f64,
58    future_value: f64,
59}
60
61#[derive(Clone, Debug)]
62pub struct TvmSeries(Vec<TvmPeriod>);
63
64/// The value of an investment at the end of a given period, part of a Time Value of Money
65/// calculation.
66///
67/// This is either:
68/// * Part of [`TvmSolution`] produced by calling [`rate_solution`], [`periods_solution`],
69/// [`present_value_solution`], or [`future_value_solution`].
70/// * Part of [`TvmSchedule`] produced by calling [`present_value_schedule`] or
71/// [`future_value_schedule`].
72#[derive(Clone, Debug)]
73pub struct TvmPeriod {
74    period: u32,
75    rate: f64,
76    value: f64,
77    formula: String,
78    symbolic_formula: String,
79}
80
81impl TvmVariable {
82    /// Returns true if the variant is TvmVariable::Rate indicating that the periodic rate was
83    /// calculated from the number of periods, the present value, and the future value.
84    pub fn is_rate(&self) -> bool {
85        match self {
86            TvmVariable::Rate => true,
87            _ => false,
88        }
89    }
90
91    /// Returns true if the variant is TvmVariable::Periods indicating that the number of periods
92    /// was calculated from the periocic rate, the present value, and the future value.
93    pub fn is_periods(&self) -> bool {
94        match self {
95            TvmVariable::Periods => true,
96            _ => false,
97        }
98    }
99
100    /// Returns true if the variant is TvmVariable::PresentValue indicating that the present value
101    /// was calculated from one or more periocic rates, the number of periods, and the future value.
102    pub fn is_present_value(&self) -> bool {
103        match self {
104            TvmVariable::PresentValue => true,
105            _ => false,
106        }
107    }
108
109    /// Returns true if the variant is TvmVariable::FutureValue indicating that the future value
110    /// was calculated from one or more periocic rates, the number of periods, and the present value.
111    pub fn is_future_value(&self) -> bool {
112        match self {
113            TvmVariable::FutureValue => true,
114            _ => false,
115        }
116    }
117
118    pub(crate) fn table_column_spec(&self, visible: bool) -> (String, String, bool) {
119        // Return something like ("period", "i") or ("rate", "r") with the column label and data
120        // type needed by a print_table() or similar function.
121        let data_type = match self {
122            TvmVariable::Periods => "i",
123            TvmVariable::Rate => "r",
124            _ => "f",
125        };
126        // We don't do anything with the visible argument except include it in the tuple. This
127        // makes the calling code simpler.
128        (self.to_string(), data_type.to_string(), visible)
129    }
130
131
132    }
133
134impl Display for TvmVariable {
135    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
136        match self {
137            TvmVariable::Rate => write!(f, "Rate"),
138            TvmVariable::Periods => write!(f, "Periods"),
139            TvmVariable::PresentValue => write!(f, "Present Value"),
140            TvmVariable::FutureValue => write!(f, "Future Value"),
141        }
142    }
143}
144
145impl Eq for TvmVariable {}
146
147impl TvmSolution {
148    pub(crate) fn new(calculated_field: TvmVariable, continuous_compounding: bool, rate: f64, periods: u32, present_value: f64, future_value: f64, formula: &str, symbolic_formula: &str) -> Self {
149        assert!(rate.is_finite());
150        assert!(present_value.is_finite());
151        assert!(future_value.is_finite());
152        assert!(!formula.is_empty());
153        assert!(!symbolic_formula.is_empty());
154        Self::new_fractional_periods(calculated_field, continuous_compounding, rate, periods as f64, present_value, future_value, formula, symbolic_formula)
155    }
156
157    pub(crate) fn new_fractional_periods(calculated_field: TvmVariable, continuous_compounding: bool, rate: f64, fractional_periods: f64, present_value: f64, future_value: f64, formula: &str, symbolic_formula: &str) -> Self {
158        assert!(rate >= -1.0);
159        assert!(fractional_periods >= 0.0);
160        assert!(present_value.is_finite());
161        assert!(future_value.is_finite());
162        assert!(formula.len() > 0);
163        assert!(symbolic_formula.len() > 0);
164        Self {
165            calculated_field,
166            continuous_compounding,
167            rate,
168            periods: round_fractional_periods(fractional_periods),
169            fractional_periods,
170            present_value,
171            future_value,
172            formula: formula.to_string(),
173            symbolic_formula: symbolic_formula.to_string(),
174        }
175    }
176
177    /// Calculates the value of an investment after each period.
178    ///
179    /// # Examples
180    /// Calculates the period-by-period details of a future value calculation. Uses
181    /// [`future_value_solution`].
182    /// ```
183    /// // The initial investment is $10,000.12, the interest rate is 1.5% per month, and the
184    /// // investment will grow for 24 months using simple compounding.
185    /// let solution = finance_solution::future_value_solution(0.015, 24, 10_000.12, false);
186    ///
187    /// // Calculate the value at the end of each period.
188    /// let series = solution.series();
189    /// dbg!(&series);
190    ///
191    /// // Confirm that we have one entry for the initial value and one entry for each period.
192    /// assert_eq!(25, series.len());
193    ///
194    /// // Print the period-by-period numbers in a formatted table.
195    /// series.print_table();
196    ///
197    /// // Create a vector with every fourth period.
198    /// let filtered_series = series
199    ///     .iter()
200    ///     .filter(|x| x.period() % 4 == 0)
201    ///     .collect::<Vec<_>>();
202    /// dbg!(&filtered_series);
203    /// assert_eq!(7, filtered_series.len());
204    /// ```
205    /// Calculate a present value with a fixed rate then examine the period-by-period values. Uses
206    /// [`present_value_solution`].
207    /// ```
208    /// // The interest rate is 7.8% per year, the investment will grow for 10 years using simple
209    /// // compounding, and the final value will be 8_112.75.
210    /// let solution = finance_solution::present_value_solution(0.078, 10, 8_112.75, false);
211    ///
212    /// // Calculate the value at the end of each period.
213    /// let series = solution.series();
214    /// dbg!(&series);
215    ///
216    /// // Confirm that we have one entry for the present value, that is the
217    /// // initial value before any interest is applied, and one entry for each
218    /// // period.
219    /// assert_eq!(11, series.len());
220    ///
221    /// // Create a reduced vector with every other period not including period 0,
222    /// // the initial state.
223    /// let filtered_series = series
224    ///     .iter()
225    ///     .filter(|x| x.period() % 2 == 0 && x.period() != 0)
226    ///     .collect::<Vec<_>>();
227    /// dbg!(&filtered_series);
228    /// assert_eq!(5, filtered_series.len());
229    /// ```
230    /// Calculate a present value with varying rates then examine the period-by-period values. Uses
231    /// [`present_value_schedule`].
232    /// ```
233    /// // The annual rate varies from -12% to 11%.
234    /// let rates = [0.04, 0.07, -0.12, -0.03, 0.11];
235    ///
236    /// // The value of the investment after applying all of these periodic rates
237    /// // will be $100_000.25.
238    /// let future_value = 100_000.25;
239    ///
240    /// // Calculate the present value and keep track of the inputs and the formula
241    /// // in a struct.
242    /// let solution = finance_solution::present_value_schedule_solution(&rates, future_value);
243    /// dbg!(&solution);
244    ///
245    /// // Calculate the value at the end of each period.
246    /// let series = solution.series();
247    /// dbg!(&series);
248    /// // There is one entry for each period and one entry for period 0 containing
249    /// // the present value.
250    /// assert_eq!(6, series.len());
251    ///
252    /// // Create a filtered list of periods, only those with a negative rate.
253    /// let filtered_series = series
254    ///     .iter()
255    ///     .filter(|x| x.rate() < 0.0)
256    ///     .collect::<Vec<_>>();
257    /// dbg!(&filtered_series);
258    /// assert_eq!(2, filtered_series.len());
259    /// ```
260    pub fn series(&self) -> TvmSeries {
261        let rates = initialized_vector(self.periods as usize, self.rate);
262        series_internal(self.calculated_field.clone(), self.continuous_compounding, &rates, self.fractional_periods, self.present_value, self.future_value)
263    }
264
265    /// Prints a formatted table with the period-by-period details of a time-value-of-money
266    /// calculation.
267    ///
268    /// Money amounts are rounded to four decimal places, rates to six places, and numbers are
269    /// formatted similar to Rust constants such as "10_000.0322". For more control over formatting
270    /// use [`TvmSolution::print_series_table_locale'].
271    ///
272    /// # Examples
273    /// ```
274    /// finance_solution::future_value_solution(0.045, 5, 10_000, false)
275    ///     .print_series_table();
276    /// ```
277    /// Output:
278    /// ```text
279    /// period      rate        value
280    /// ------  --------  -----------
281    ///      0  0.000000  10_000.0000
282    ///      1  0.045000  10_450.0000
283    ///      2  0.045000  10_920.2500
284    ///      3  0.045000  11_411.6612
285    ///      4  0.045000  11_925.1860
286    ///      5  0.045000  12_461.8194
287    /// ```
288    pub fn print_series_table(&self) {
289        self.series().print_table();
290    }
291
292    /// Prints a formatted table with the period-by-period details of a time-value-of-money
293    /// calculation.
294    ///
295    /// For a simpler function that doesn't require a locale use
296    /// [`TvmSolution::print_series_table'].
297    ///
298    /// # Arguments
299    /// * `locale` - A locale constant from the `num-format` crate such as `Locale::en` for English
300    /// or `Locale::vi` for Vietnamese. The locale determines the thousands separator and decimal
301    /// separator.
302    /// * `precision` - The number of decimal places for money amounts. Rates will appear with at
303    /// least six places regardless of this argument.
304    ///
305    /// # Examples
306    /// ```
307    /// // English formatting with "," for the thousands separator and "." for the decimal
308    /// // separator.
309    /// let locale = finance_solution::num_format::Locale::en;
310    ///
311    /// // Show money amounts to two decimal places.
312    /// let precision = 2;
313    ///
314    /// finance_solution::future_value_solution(0.11, 4, 5_000, false)
315    ///     .print_series_table_locale(&locale, precision);
316    /// ```
317    /// Output:
318    /// ```text
319    /// period      rate     value
320    /// ------  --------  --------
321    ///      0  0.000000  5,000.00
322    ///      1  0.110000  5,550.00
323    ///      2  0.110000  6,160.50
324    ///      3  0.110000  6,838.16
325    ///      4  0.110000  7,590.35
326    /// ```
327    pub fn print_series_table_locale(&self, locale: &num_format::Locale, precision: usize) {
328        self.series().print_table_locale(locale, precision);
329    }
330
331    /// Returns a variant of [`TvmVariable`] showing which value was calculated, either the periodic
332    /// rate, number of periods, present value, or future value. To test for the enum variant use
333    /// functions like `TvmVariable::is_rate`.
334    ///
335    /// # Examples
336    /// ```
337    /// // Calculate the future value of $25,000 that grows at 5% for 12 yeors.
338    /// let solution = finance_solution::future_value_solution(0.05, 12, 25_000, false);
339    /// assert!(solution.calculated_field().is_future_value());
340    /// ```
341    pub fn calculated_field(&self) -> &TvmVariable {
342        &self.calculated_field
343    }
344
345    /// Returns true if the value is compounded continuously rather than period-by-period.
346    pub fn continuous_compounding(&self) -> bool {
347        self.continuous_compounding
348    }
349
350    /// Returns the periodic rate which is a calculated value if this `TvmSolution` struct is the
351    /// result of a call to [`rate_solution`] and otherwise is one of the input values.
352    pub fn rate(&self) -> f64 {
353        self.rate
354    }
355
356    /// Returns the number of periods as a whole number. This is a calculated value if this
357    /// `TvmSolution` struct is the result of a call to [`periods_solution`] and otherwise it's
358    /// one of the input values. If the value was calculated the true result may not have been a
359    /// whole number so this is that number rounded away from zero.
360    pub fn periods(&self) -> u32 {
361        self.periods
362    }
363
364    /// Returns the number of periods as a floating point number. This is a calculated value if this
365    /// `TvmSolution` struct is the result of a call to [`periods_solution`] and otherwise it's
366    /// one of the input values.
367    pub fn fractional_periods(&self) -> f64 {
368        self.fractional_periods
369    }
370
371    /// Returns the present value which is a calculated value if this `TvmSolution` struct is the
372    /// result of a call to [`present_value_solution`] and otherwise is one of the input values.
373    pub fn present_value(&self) -> f64 {
374        self.present_value
375    }
376
377    /// Returns the future value which is a calculated value if this `TvmSolution` struct is the
378    /// result of a call to [`future_value_solution`] and otherwise is one of the input values.
379    pub fn future_value(&self) -> f64 {
380        self.future_value
381    }
382
383    /// Returns a text version of the formula used to calculate the result which may have been the
384    /// periodic rate, number of periods, present value, or future value depending on which function
385    /// was called. The formula includes the actual values rather than variable names. For the
386    /// formula with variables such as r for rate call [symbolic_formula](./struct.TvmSolution.html#method.symbolic_formula).
387    pub fn formula(&self) -> &str {
388        &self.formula
389    }
390
391    /// Returns a text version of the formula used to calculate the result which may have been the
392    /// periodic rate, number of periods, present value, or future value depending on which function
393    /// was called. The formula uses variables such as n for the number of periods. For the formula
394    /// with the actual values rather than variables call [formula](./struct.TvmSolution.html#method.formula).
395    pub fn symbolic_formula(&self) -> &str {
396        &self.symbolic_formula
397    }
398    
399    pub fn rate_solution(&self, continuous_compounding: bool, compounding_periods: Option<u32>) -> TvmSolution {
400        let periods= compounding_periods.unwrap_or(self.periods);
401        rate_solution_internal(periods, self.present_value, self.future_value, continuous_compounding)
402    }
403
404    pub fn periods_solution(&self, continuous_compounding: bool) -> TvmSolution {
405        periods_solution_internal(self.rate, self.present_value, self.future_value, continuous_compounding)
406    }
407
408    pub fn present_value_solution(&self, continuous_compounding: bool, compounding_periods: Option<u32>) -> TvmSolution {
409        let (rate, periods) = match compounding_periods {
410            Some(periods) => ((self.rate * self.fractional_periods) / periods as f64, periods as f64),
411            None => (self.rate, self.fractional_periods),
412        };
413        present_value_solution_internal(rate, periods, self.future_value, continuous_compounding)
414    }
415
416    pub fn future_value_solution(&self, continuous_compounding: bool, compounding_periods: Option<u32>) -> TvmSolution {
417        let (rate, periods) = match compounding_periods {
418            Some(periods) => ((self.rate * self.fractional_periods) / periods as f64, periods as f64),
419            None => (self.rate, self.fractional_periods),
420        };
421        future_value_solution_internal(rate, periods, self.present_value, continuous_compounding)
422    }
423
424    /// Returns a struct with a set of what-if scenarios for the present value needed with a variety
425    /// of compounding periods.
426    ///
427    /// # Arguments
428    /// * `compounding_periods` - The compounding periods to include in the scenarios. The result
429    /// will have a computed present value for each compounding period in this list.
430    /// * `include_continuous_compounding` - If true, adds one scenario at the end of the results
431    /// with continuous compounding instead of a given number of compounding periods.
432    ///
433    /// # Examples
434    /// For a more detailed example with a related function see
435    /// [future_value_vary_compounding_periods](./struct.TVMoneySolution.html#method.future_value_vary_compounding_periods)
436    /// ```
437    /// // Calculate the future value of an investment that starts at $83.33 and grows 20% in one
438    /// // year using simple compounding. Note that we're going to examine how the present value
439    /// // varies by the number of compounding periods but we're starting with a future value
440    /// // calculation. It would have been fine to start with a rate, periods, or present value
441    /// // calculation as well. It just depends on what information we have to work with.
442    /// let solution = finance_solution::future_value_solution(0.20, 1, -83.333, false);
443    /// dbg!(&solution);
444    ///
445    /// // The present value of $83.33 gives us a future value of about $100.00.
446    /// finance_solution::assert_rounded_2!(100.00, solution.future_value());
447    ///
448    /// // We'll experiment with compounding annually, quarterly, monthly, weekly, and daily.
449    /// let compounding_periods = [1, 4, 12, 52, 365];
450    ///
451    /// // Add a final scenario with continuous compounding.
452    /// let include_continuous_compounding = true;
453    ///
454    /// // Compile a list of the present values needed to arrive at the calculated future value of $100
455    /// // each of the above compounding periods as well a continous compounding.
456    /// let scenarios = solution.present_value_vary_compounding_periods(&compounding_periods, include_continuous_compounding);
457    /// dbg!(&scenarios);
458    ///
459    /// // Print the results in a formatted table.
460    /// scenarios.print_table();
461    ///
462    /// ```
463    /// Output from the last line:
464    /// ```text
465    /// Periods  Present Value
466    /// -------  -------------
467    ///       1        83.3330
468    ///       4        82.2699
469    ///      12        82.0078
470    ///      52        81.9042
471    ///     365        81.8772
472    ///     inf        81.8727
473    /// ```
474    /// As we compound the interest more frequently we need a slightly smaller initial value to
475    /// reach the same final value of $100 in one year. With more frequent compounding the required
476    /// initial value approaches $81.87, the present value needed with continuous compounding.
477    ///
478    /// If we plot this using between 1 and 12 compounding periods it's clear that the required
479    /// present value drops sharply if we go from compounding annually to compounding semiannually
480    /// or quarterly but then is affected less and less as we compound more frequently:
481    ///
482    /// <img src="http://i.upmath.me/svg/%24%24%5Cbegin%7Btikzpicture%7D%5Bscale%3D1.0544%5D%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%3D80.5%2C%20ymax%3D84.5%2C%0A%09restrict%20y%20to%20domain%3D0%3A1000%2C%0A%09ytick%3D%7B81%2C%2082%2C%2083%2C%2084%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%24pv%24%5D%0A%5Caddplot%5Bblue%2Cdomain%3D1%3A12%2Csemithick%2C%20only%20marks%5D%7B100%2F((1%2B(0.2%2Fx))%5Ex)%7D%3B%0A%5Caddplot%5Bblack%2Cdomain%3D1%3A12%2C%20thick%5D%7B100%2F(e%5E(0.2))%7D%3B%0A%5Caddplot%5B%5D%20coordinates%20%7B(2.3%2C81.53)%7D%20node%7B%24pv%3D%7B100%20%5Cover%20e%5E%7B0.2%7D%7D%24%7D%3B%0A%5Caddplot%5Bblue%5D%20coordinates%20%7B(4.5%2C82.8)%7D%20node%7B%24pv%3D%7B100%20%5Cover%20(1%2B%7B0.2%20%5Cover%20n%7D)%5En%7D%24%7D%3B%0A%5Cpath%20(axis%20cs%3A0%2C83)%20node%20%5Banchor%3Dnorth%20west%2Cyshift%3D-0.07cm%5D%3B%0A%5Cend%7Baxis%7D%0A%5Cend%7Btikzpicture%7D%24%24" />
483    pub fn present_value_vary_compounding_periods(&self, compounding_periods: &[u32], include_continuous_compounding: bool) -> ScenarioList {
484        let rate_for_single_period = self.rate * self.fractional_periods;
485        let mut entries = vec![];
486        for periods in compounding_periods {
487            let rate = rate_for_single_period / *periods as f64;
488            let present_value = present_value_internal(rate, *periods as f64, self.future_value, self.continuous_compounding);
489            entries.push((*periods as f64, present_value));
490        }
491        if include_continuous_compounding {
492            let rate = rate_for_single_period;
493            let periods = 1;
494            let continuous_compounding = true;
495            let present_value = present_value_internal(rate, periods as f64, self.future_value, continuous_compounding);
496            entries.push((std::f64::INFINITY, present_value));
497        }
498
499        let setup = format!("Compare present values with different compounding periods where the rate is {} and the future value is {}.", format_rate(rate_for_single_period), format_float(self.future_value));
500        ScenarioList::new(setup, TvmVariable::Periods, TvmVariable::PresentValue, entries)
501    }
502
503    /// Returns a struct with a set of what-if scenarios for the future value of an investment given
504    /// a variety of compounding periods.
505    ///
506    /// # Arguments
507    /// * `compounding_periods` - The compounding periods to include in the scenarios. The result
508    /// will have a computed future value for each compounding period in this list.
509    /// * `include_continuous_compounding` - If true, adds one scenario at the end of the results
510    /// with continuous compounding instead of a given number of compounding periods.
511    ///
512    /// # Examples
513    /// ```
514    /// // The interest rate is 5% per quarter.
515    /// let rate = 0.05;
516    ///
517    /// // The interest will be applied once per quarter for one year.
518    /// let periods = 4;
519    ///
520    /// // The starting value is $100.00.
521    /// let present_value = 100;
522    ///
523    /// let continuous_compounding = false;
524    ///
525    /// let solution = finance_solution::future_value_solution(rate, periods, present_value, continuous_compounding);
526    /// dbg!(&solution);
527    ///
528    /// // We'll experiment with compounding annually, quarterly, monthly, weekly, and daily.
529    /// let compounding_periods = [1, 4, 12, 52, 365];
530    ///
531    /// // Add a final scenario with continuous compounding.
532    /// let include_continuous_compounding = true;
533    ///
534    /// // Compile a list of the future values with each of the above compounding periods as well as
535    /// // continous compounding.
536    /// let scenarios = solution.future_value_vary_compounding_periods(&compounding_periods, include_continuous_compounding);
537    /// // The description in the `setup` field states that the rate is 20% since that's 5% times the
538    /// // number of periods in the original calculation. The final entry has `input: inf` indicating
539    /// // that we used continuous compounding.
540    /// dbg!(&scenarios);
541    ///
542    /// // Print the results in a formatted table.
543    /// scenarios.print_table();
544    /// ```
545    /// Output:
546    /// ```text
547    /// &solution = FutureValueSolution {
548    ///     tvm_solution: TvmSolution {
549    ///     calculated_field: FutureValue,
550    ///     continuous_compounding: false,
551    ///     rate: 0.05,
552    ///     periods: 4,
553    ///     fractional_periods: 4.0,
554    ///     present_value: 100.0,
555    ///     future_value: 121.55062500000003,
556    ///     formula: "121.5506 = 100.0000 * (1.050000 ^ 4)",
557    ///     symbolic_formula: "fv = pv * (1 + r)^n",
558    /// },
559    ///
560    /// &scenarios = ScenarioList {
561    ///     setup: "Compare future values with different compounding periods where the rate is 0.200000 and the present value is 100.0000.",
562    ///     input_variable: Periods,
563    ///     output_variable: FutureValue,
564    ///     entries: [
565    ///         { input: 1, output: 120.0000 },
566    ///         { input: 4, output: 121.5506 },
567    ///         { input: 12, output: 121.9391 },
568    ///         { input: 52, output: 122.0934 },
569    ///         { input: 365, output: 122.1336 },
570    ///         { input: inf, output: 122.1403 },
571    ///     ],
572    /// }
573    ///
574    /// Periods  Future Value
575    /// -------  ------------
576    ///       1      120.0000
577    ///       4      121.5506
578    ///      12      121.9391
579    ///      52      122.0934
580    ///     365      122.1336
581    ///     inf      122.1403
582    /// ```
583    /// With the same interest rate and overall time period, an amount grows faster if we compound
584    /// the interest more frequently. As the number of compounding periods grows the future value
585    /// approaches the limit of $122.14 that we get with continuous compounding.
586    ///
587    /// As a chart it looks like this, here using only 1 through 12
588    /// compounding periods for clarity:
589    ///
590    /// <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%3D119%2C%20ymax%3D123%2C%0A%09restrict%20y%20to%20domain%3D0%3A1000%2C%0A%09ytick%3D%7B120%2C%20121%2C%20122%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%2Cthick%2C%20only%20marks%5D%7B100*((1%2B(0.2%2Fx))%5Ex)%7D%3B%0A%5Caddplot%5Bblack%2Cdomain%3D1%3A12%2Cthick%5D%7B100*(e%5E(0.2))%7D%3B%0A%5Caddplot%5B%5D%20coordinates%20%7B(2.5%2C122.4)%7D%20node%7B%24fv%3D100e%5E%7B0.2%7D%24%7D%3B%0A%5Caddplot%5Bblue%5D%20coordinates%20%7B(4.8%2C120.7)%7D%20node%7B%24fv%3D100(1%2B%7B0.2%20%5Cover%20n%7D)%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" />
591    pub fn future_value_vary_compounding_periods(&self, compounding_periods: &[u32], include_continuous_compounding: bool) -> ScenarioList {
592        let rate_for_single_period = self.rate * self.fractional_periods;
593        let mut entries = vec![];
594        for periods in compounding_periods {
595            let rate = rate_for_single_period / *periods as f64;
596            let future_value = future_value_internal(rate, *periods as f64, self.present_value, self.continuous_compounding);
597            entries.push((*periods as f64, future_value));
598        }
599        if include_continuous_compounding {
600            let rate = rate_for_single_period;
601            let periods = 1;
602            let continuous_compounding = true;
603            let future_value = future_value_internal(rate, periods as f64, self.present_value, continuous_compounding);
604            entries.push((std::f64::INFINITY, future_value));
605        }
606
607        let setup = format!("Compare future values with different compounding periods where the rate is {} and the present value is {}.", format_rate(rate_for_single_period), format_float(self.present_value));
608        ScenarioList::new(setup, TvmVariable::Periods, TvmVariable::FutureValue, entries)
609    }
610
611    pub fn print_ab_comparison(
612        &self,
613        other: &TvmSolution)
614    {
615        self.print_ab_comparison_locale_opt(other, None, None);
616    }
617
618    pub fn print_ab_comparison_locale(
619        &self,
620        other: &TvmSolution,
621        locale: &num_format::Locale,
622        precision: usize)
623    {
624        self.print_ab_comparison_locale_opt(other, Some(locale), Some(precision));
625    }
626
627    fn print_ab_comparison_locale_opt(
628        &self,
629        other: &TvmSolution,
630        locale: Option<&num_format::Locale>,
631        precision: Option<usize>)
632    {
633        println!();
634        print_ab_comparison_values_string("calculated_field", &self.calculated_field.to_string(), &other.calculated_field.to_string());
635        print_ab_comparison_values_bool("continuous_compounding", self.continuous_compounding, other.continuous_compounding);
636        print_ab_comparison_values_rate("rate", self.rate, other.rate, locale, precision);
637        print_ab_comparison_values_int("periods", self.periods as i128, other.periods as i128, locale);
638        if self.calculated_field.is_periods() {
639            print_ab_comparison_values_float("fractional_periods", self.fractional_periods, other.fractional_periods, locale, precision);
640        }
641        print_ab_comparison_values_float("present_value", self.present_value, other.present_value, locale, precision);
642        print_ab_comparison_values_float("future_value", self.future_value, other.future_value, locale, precision);
643        print_ab_comparison_values_string("formula", &self.formula, &other.formula);
644        print_ab_comparison_values_string("symbolic_formula", &self.symbolic_formula, &other.symbolic_formula);
645
646        self.series().print_ab_comparison_locale_opt(&other.series(), locale, precision);
647    }
648
649    pub(crate) fn invariant(&self) {
650        assert!(self.rate.is_finite());
651        assert!(self.fractional_periods.is_finite());
652        assert_eq!(self.periods, round_fractional_periods(self.fractional_periods));
653        assert!(self.present_value.is_finite());
654        assert!(self.future_value.is_finite());
655        assert!(!self.formula.is_empty());
656        assert!(!self.symbolic_formula.is_empty());
657    }
658}
659
660impl PartialEq for TvmSolution {
661    fn eq(&self, other: &Self) -> bool {
662        self.calculated_field == other.calculated_field
663            && self.continuous_compounding == other.continuous_compounding
664            && is_approx_equal!(self.rate, other.rate)
665            && self.periods == other.periods
666            && is_approx_equal!(self.fractional_periods, other.fractional_periods)
667            && is_approx_equal!(self.present_value, other.present_value)
668            && is_approx_equal!(self.future_value, other.future_value)
669            && self.formula == other.formula
670            && self.symbolic_formula == other.symbolic_formula
671    }
672}
673
674impl TvmScheduleSolution {
675    pub(crate) fn new(calculated_field: TvmVariable, rates: &[f64], present_value: f64, future_value: f64) -> Self {
676        for rate in rates.iter() {
677            assert!(rate.is_finite());
678        }
679        assert!(present_value.is_finite());
680        assert!(future_value.is_finite());
681        Self {
682            calculated_field,
683            rates: rates.to_vec(),
684            periods: rates.len() as u32,
685            present_value,
686            future_value,
687        }
688    }
689
690    /// Returns a variant of [`TvmVariable`] showing which value was calculated, either the present
691    /// value or the future value. To test for the enum variant use functions like
692    /// `TvmVariable::is_future_value`.
693    ///
694    /// # Examples
695    /// ```
696    /// let solution = finance_solution::present_value_schedule_solution(&[0.011, 0.012, 0.009], 75_000);
697    /// assert!(solution.calculated_field().is_present_value());
698    /// ```
699    pub fn calculated_field(&self) -> &TvmVariable {
700        &self.calculated_field
701    }
702
703    /// Returns the periodic rates that were passed to the function.
704    pub fn rates(&self) -> &[f64] {
705        &self.rates
706    }
707
708    /// Returns the number of periods which was derived from the number of rates passed to the
709    /// function.
710    ///
711    /// # Examples
712    /// ```
713    /// let solution = finance_solution::future_value_schedule_solution(&[0.05, 0.07, 0.05], 100_000);
714    /// assert_eq!(3, solution.periods());
715    /// ```
716    pub fn periods(&self) -> u32 {
717        self.periods
718    }
719
720    /// Returns the present value which is a calculated value if this `TvmSchedule` struct is the
721    /// result of a call to [`present_value_schedule_solution`] and otherwise is one of the input
722    /// values.
723    pub fn present_value(&self) -> f64 {
724        self.present_value
725    }
726
727    /// Returns the future value which is a calculated value if this `TvmSchedule` struct is the
728    /// result of a call to [`future_value_schedule_solution`] and otherwise is one of the input
729    /// values.
730    pub fn future_value(&self) -> f64 {
731        self.future_value
732    }
733
734    /// Calculates the value of an investment after each period.
735    ///
736    /// # Examples
737    /// Calculate the period-by-period details of a future value calculation. Uses
738    /// [`future_value_solution`].
739    /// ```
740    /// // The initial investment is $10,000.12, the interest rate is 1.5% per month, and the
741    /// // investment will grow for 24 months using simple compounding.
742    /// let solution = finance_solution::future_value_solution(0.015, 24, 10_000.12, false);
743    /// dbg!(&solution);
744    ///
745    /// // Calculate the period-by-period details.
746    /// let series = solution.series();
747    /// dbg!(&series);
748    ///
749    /// // Confirm that we have one entry for the initial value and one entry for each period.
750    /// assert_eq!(25, series.len());
751    ///
752    /// // Print the period-by-period numbers in a formatted table.
753    /// series.print_table();
754    ///
755    /// // Create a vector with every fourth period.
756    /// let filtered_series = series
757    ///     .iter()
758    ///     .filter(|x| x.period() % 4 == 0)
759    ///     .collect::<Vec<_>>();
760    /// dbg!(&filtered_series);
761    /// assert_eq!(7, filtered_series.len());
762    /// ```
763    pub fn series(&self) -> TvmSeries {
764        series_internal(self.calculated_field.clone(), false, &self.rates,0.0, self.present_value, self.future_value)
765    }
766
767    pub(crate) fn invariant(&self) {
768        for rate in self.rates.iter() {
769            assert!(rate.is_finite());
770        }
771        assert!(self.present_value.is_finite());
772        assert!(self.future_value.is_finite());
773    }
774}
775
776impl TvmSeries {
777    pub(crate) fn new(series: Vec<TvmPeriod>) -> Self {
778        Self {
779            0: series,
780        }
781    }
782
783    pub fn filter<P>(&self, predicate: P) -> Self
784        where P: Fn(&&TvmPeriod) -> bool
785    {
786        Self {
787            0: self.iter().filter(|x| predicate(x)).cloned().collect()
788        }
789    }
790
791    pub fn print_table(&self) {
792        self.print_table_locale_opt(None, None);
793    }
794
795    pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
796        self.print_table_locale_opt(Some(locale), Some(precision));
797    }
798
799    fn print_table_locale_opt(&self, locale: Option<&num_format::Locale>, precision: Option<usize>) {
800        let columns = columns_with_strings(&[("period", "i", true), ("rate", "r", true), ("value", "f", true)]);
801        let data = self.iter()
802            .map(|entry| vec![entry.period.to_string(), entry.rate.to_string(), entry.value.to_string()])
803            .collect::<Vec<_>>();
804        print_table_locale_opt(&columns, data, locale, precision);
805    }
806
807    pub fn print_ab_comparison(
808        &self,
809        other: &TvmSeries)
810    {
811        self.print_ab_comparison_locale_opt(other, None, None);
812    }
813
814    pub fn print_ab_comparison_locale(
815        &self,
816        other: &TvmSeries,
817        locale: &num_format::Locale,
818        precision: usize)
819    {
820        self.print_ab_comparison_locale_opt(other, Some(locale), Some(precision))
821    }
822
823    fn print_ab_comparison_locale_opt(
824        &self,
825        other: &TvmSeries,
826        locale: Option<&num_format::Locale>,
827        precision: Option<usize>)
828    {
829        let columns = columns_with_strings(&[("period", "i", true),
830                           ("rate_a", "r", true), ("rate_b", "r", true),
831                           ("value_a", "f", true), ("value_b", "f", true)]);
832        let mut data = vec![];
833        let rows = max(self.len(), other.len());
834        for row_index in 0..rows {
835            data.push(vec![
836                row_index.to_string(),
837                self.get(row_index).map_or("".to_string(), |x| x.rate.to_string()),
838                other.get(row_index).map_or("".to_string(), |x| x.rate.to_string()),
839                self.get(row_index).map_or("".to_string(), |x| x.value.to_string()),
840                other.get(row_index).map_or("".to_string(), |x| x.value.to_string()),
841            ]);
842        }
843        print_table_locale_opt(&columns, data, locale, precision);
844    }
845}
846
847impl Deref for TvmSeries{
848    type Target = Vec<TvmPeriod>;
849
850    fn deref(&self) -> &Self::Target {
851        &self.0
852    }
853}
854
855impl TvmPeriod {
856    pub(crate) fn new(period: u32, rate: f64, value: f64, formula: &str, symbolic_formula: &str) -> Self {
857        assert!(rate.is_finite());
858        assert!(value.is_finite());
859        assert!(!formula.is_empty());
860        assert!(!symbolic_formula.is_empty());
861        Self {
862            period,
863            rate,
864            value,
865            formula: formula.to_string(),
866            symbolic_formula: symbolic_formula.to_string()
867        }
868    }
869
870    /// Returns the period number. The first real period is 1 but there's also a period 0 which
871    /// shows the starting conditions.
872    pub fn period(&self) -> u32 {
873        self.period
874    }
875
876    /// Returns the periodic rate for the current period. If the containing struct is a
877    /// [`TvmSolution`] every period will have the same rate. If it's a [`TvmSchedule`] each period
878    /// may have a different rate.
879    pub fn rate(&self) -> f64 {
880        self.rate
881    }
882
883    /// Returns the value of the investment at the end of the current period.
884    pub fn value(&self) -> f64 {
885        self.value
886    }
887
888    /// Returns a text version of the formula used to calculate the value for the current period.
889    /// The formula includes the actual values rather than variable names. For the formula with
890    /// variables such as pv for present value call `symbolic_formula`.
891    pub fn formula(&self) -> &str {
892        &self.formula
893    }
894
895    /// Returns a text version of the formula used to calculate the value for the current period.
896    /// The formula includes variables such as r for the rate. For the formula with actual values
897    /// rather than variables call `formula`.
898    pub fn symbolic_formula(&self) -> &str {
899        &self.symbolic_formula
900    }
901}
902
903/*
904impl Debug for TvmPeriod {
905    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
906        write!(f, "{{ {}, {}, {}, {}, {} }}",
907               &format!("period: {}", self.period),
908               &format!("rate: {:.6}", self.rate),
909               &format!("value: {:.4}", self.value),
910               &format!("formula: {:?}", self.formula),
911               &format!("symbolic_formula: {:?}", self.symbolic_formula),
912        )
913    }
914}
915*/
916
917fn series_internal(
918    calculated_field: TvmVariable,
919    continuous_compounding: bool,
920    rates: &[f64],
921    _fractional_periods: f64,
922    present_value: f64,
923    future_value: f64,
924) -> TvmSeries {
925    let periods = rates.len();
926    let mut series = vec![];
927    if calculated_field.is_present_value() {
928        // next_value refers to the value of the period following the current one in the loop.
929        let mut next_value = None;
930
931        // Add the values at each period.
932        // Start at the last period since we calculate each period's value from the following period,
933        // except for the last period which simply has the future value. We'll have a period 0
934        // representing the present value.
935        for period in (0..=periods).rev() {
936            let one_rate = if period == 0 {
937                0.0
938            } else {
939                rates[period - 1]
940            };
941            assert!(one_rate.is_finite());
942            assert!(one_rate >= -1.0);
943
944            // let rate_multiplier = 1.0 + one_rate;
945
946            let (value, formula, symbolic_formula) = if period == periods {
947                // This was a present value calculation so we started with a given future value. The
948                // value at the end of the last period is simply the future value.
949                let value = future_value;
950                let formula = format!("{:.4}", value);
951                let symbolic_formula = "value = fv";
952                (value, formula, symbolic_formula)
953            } else {
954                // Since this was a present value calculation we started with the future value, that is
955                // the value at the end of the last period. Here we're working with some period other
956                // than the last period so we calculate this period's value based on the period after
957                // it.
958                let rate_next_period = rates[period];
959                if continuous_compounding {
960                    let value = next_value.unwrap() / std::f64::consts::E.powf(rate_next_period);
961                    let formula = format!("{:.4} = {:.4} / ({:.6} ^ {:.6})", value, next_value.unwrap(), std::f64::consts::E, rate_next_period);
962                    let symbolic_formula = "pv = fv / e^r";
963                    (value, formula, symbolic_formula)
964                } else {
965                    let rate_multiplier_next_period = 1.0 + rate_next_period;
966                    let value = next_value.unwrap() / rate_multiplier_next_period;
967                    let formula = format!("{:.4} = {:.4} / {:.6}", value, next_value.unwrap(), rate_multiplier_next_period);
968                    let symbolic_formula = "value = {next period value} / (1 + r)";
969                    (value, formula, symbolic_formula)
970                }
971            };
972            assert!(value.is_finite());
973            next_value = Some(value);
974            // We want to end up with the periods in order so for each pass through the loop insert the
975            // current TvmPeriod at the beginning of the vector.
976            series.insert(0, TvmPeriod::new(period as u32, one_rate, value, &formula, symbolic_formula))
977        }
978    } else {
979        // For a rate, periods, or future value calculation the the period-by-period values are
980        // calculated the same way, starting with the present value and multiplying the value by
981        // (1 + rate) for each period. The only nuance is that if we got here from a periods
982        // calculation the last period may not be a full one, so there is some special handling of
983        // the formulas and values.
984
985        // For each period after 0, prev_value will hold the value of the previous period.
986        let mut prev_value = None;
987
988        // Add the values at each period.
989        for period in 0..=periods {
990            let one_rate = if period == 0 {
991                0.0
992            } else {
993                rates[period - 1]
994            };
995            assert!(one_rate.is_finite());
996            assert!(one_rate >= -1.0);
997
998            let rate_multiplier = 1.0 + one_rate;
999            assert!(rate_multiplier.is_finite());
1000            assert!(rate_multiplier >= 0.0);
1001
1002            let (value, formula, symbolic_formula) = if period == 0 {
1003                let value = -present_value;
1004                let formula = format!("{:.4}", value);
1005                let symbolic_formula = "value = pv";
1006                (value, formula, symbolic_formula)
1007            } else if calculated_field.is_periods() && period == periods {
1008                // We calculated periods and this may not be a whole number, so for the last
1009                // period use the future value. If instead we multiplied the previous
1010                // period's value by (1 + rate) we could overshoot the future value.
1011                let value = future_value;
1012                let formula = format!("{:.4}", value);
1013                let symbolic_formula = "value = fv";
1014                (value, formula, symbolic_formula)
1015            } else {
1016                // The usual case.
1017                if continuous_compounding {
1018                    let value = prev_value.unwrap() * std::f64::consts::E.powf(one_rate);
1019                    let formula = format!("{:.4} = {:.4} * ({:.6} ^ {:.6})", value, prev_value.unwrap(), std::f64::consts::E, one_rate);
1020                    let symbolic_formula = "fv = pv * e^r";
1021                    (value, formula, symbolic_formula)
1022                } else {
1023                    let value = prev_value.unwrap() * rate_multiplier;
1024                    let formula = format!("{:.4} = {:.4} * {:.6}", value, prev_value.unwrap(), rate_multiplier);
1025                    let symbolic_formula = "value = {previous period value} * (1 + r)";
1026                    (value, formula, symbolic_formula)
1027                }
1028            };
1029            assert!(value.is_finite());
1030            prev_value = Some(value);
1031            series.push(TvmPeriod::new(period as u32, one_rate, value, &formula, symbolic_formula))
1032        }
1033    }
1034    TvmSeries::new(series)
1035}
1036
1037
1038fn round_fractional_periods(fractional_periods: f64) -> u32 {
1039    round_4(fractional_periods).ceil() as u32
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044    use super::*;
1045
1046    #[test]
1047    fn test_tvm_symmetry_one() {
1048        let rate = 0.10;
1049        let periods = 4;
1050        let present_value = -5_000.00;
1051        check_symmetry(rate, periods, present_value);
1052    }
1053
1054    #[test]
1055    fn test_tvm_symmetry_multiple() {
1056        let rates = vec![-1.0, -0.5, -0.05, -0.005, 0.0, 0.005, 0.05, 0.5, 1.0, 10.0, 100.0];
1057        // let rates = vec![-0.5, -0.05, -0.005, 0.0, 0.005, 0.05, 0.5, 1.0, 10.0, 100.0];
1058        // let periods: Vec<u32> = vec![0, 1, 2, 5, 10, 36, 100, 1_000];
1059        let periods: Vec<u32> = vec![0, 1, 2, 5, 10, 36];
1060        let present_values: Vec<f64> = vec![-1_000_000.0, -1_234.98, -1.0, 0.0, 5.55555, 99_999.99];
1061        for rate_one in rates.iter() {
1062            for periods_one in periods.iter() {
1063                for present_value_one in present_values.iter() {
1064                    if !(*periods_one > 50 && *rate_one > 0.01) {
1065                        if !(*periods_one == 0 && *present_value_one != 0.0) {
1066                            check_symmetry(*rate_one, *periods_one, *present_value_one);
1067                        }
1068                    }
1069                }
1070            }
1071        }
1072    }
1073
1074    fn check_symmetry(rate_in: f64, periods_in: u32, present_value_in: f64) {
1075        //bg!("check_symmetry", rate_in, periods_in, present_value_in);
1076
1077        // Calculate the future value given the other three inputs so that we have all four values
1078        // which we can use in various combinations to confirm that all four basic TVM functions
1079        // return consistent values.
1080        let future_value_calc = future_value(rate_in, periods_in, present_value_in, false);
1081        //bg!(future_value_calc);
1082        //bg!(future_value_calc.is_normal());
1083
1084        let rate_calc = rate(periods_in, present_value_in, future_value_calc, false);
1085        //bg!(rate_calc);
1086        if periods_in == 0 || present_value_in == 0.0 {
1087            // With zero periods or zero for the present value, presumably the future value is the
1088            // same as the present value and any periodic rate would be fine so we arbitrarily
1089            // return zero.
1090            assert_approx_equal_symmetry_test!(present_value_in, future_value_calc);
1091            assert_approx_equal_symmetry_test!(0.0, rate_calc);
1092        } else {
1093            //bg!(rate_calc, rate_in);
1094            assert_approx_equal_symmetry_test!(rate_calc, rate_in);
1095        }
1096
1097        let fractional_periods_calc = periods(rate_in, present_value_in, future_value_calc, false);
1098        //bg!(fractional_periods_calc);
1099        let periods_calc = round_4(fractional_periods_calc).ceil() as u32;
1100        //bg!(periods_calc);
1101        if rate_in == 0.0 || present_value_in == 0.0 || periods_in == 0 {
1102            // If the rate is zero or the present value is zero then the present value and future
1103            // value will be the same (but with opposite signs) and periods() will return zero since
1104            // no periods are required.
1105            assert_approx_equal_symmetry_test!(present_value_in, -future_value_calc);
1106            assert_eq!(0, periods_calc);
1107        } else if rate_in == -1.0 {
1108            // The investment will drop to zero by the end of the first period so periods() will
1109            // return 1.
1110            assert_approx_equal_symmetry_test!(0.0, future_value_calc);
1111            assert_eq!(1, periods_calc);
1112        } else {
1113            // This is the normal case and we expect periods() to return the same number of periods
1114            // we started with.
1115            assert_eq!(periods_calc, periods_in);
1116        }
1117
1118        if future_value_calc.is_normal() {
1119            let present_value_calc = present_value(rate_in, periods_in, future_value_calc, false);
1120            //bg!(present_value_calc);
1121            assert_approx_equal_symmetry_test!(present_value_calc, present_value_in);
1122        };
1123
1124        // Create a list of rates that are all the same so that we can try the _schedule functions
1125        // For present value and future value
1126        let mut rates_in = vec![];
1127        for _ in 0..periods_in {
1128            rates_in.push(rate_in);
1129        }
1130
1131        if future_value_calc.is_normal() {
1132            let present_value_schedule_calc = present_value_schedule(&rates_in, future_value_calc);
1133            //bg!(present_value_schedule_calc);
1134            assert_approx_equal_symmetry_test!(present_value_schedule_calc, present_value_in);
1135        }
1136
1137        let future_value_schedule_calc = future_value_schedule(&rates_in, present_value_in);
1138        //bg!(future_value_schedule_calc);
1139        assert_approx_equal_symmetry_test!(future_value_schedule_calc, future_value_calc);
1140
1141        // Create TvmSolution structs by solving for each of the four possible variables.
1142        let mut solutions = vec![
1143            rate_solution(periods_in, present_value_in, future_value_calc, false),
1144            periods_solution(rate_in, present_value_in, future_value_calc, false),
1145            future_value_solution(rate_in, periods_in, present_value_in, false),
1146        ];
1147
1148        if future_value_calc.is_normal() {
1149            solutions.push(present_value_solution(rate_in, periods_in, future_value_calc, false));
1150        }
1151        for solution in solutions.iter() {
1152            //bg!(solution);
1153            if solution.calculated_field().is_rate() {
1154                // There are a few special cases in which the calculated rate is arbitrarily set to
1155                // zero since any value would work. We've already checked rate_calc against those
1156                // special cases, so use that here for the comparison.
1157                if !is_approx_equal_symmetry_test!(rate_calc, solution.rate()) {
1158                    dbg!(rate_calc, solution.rate(), &solution);
1159                }
1160                assert_approx_equal_symmetry_test!(rate_calc, solution.rate());
1161            } else {
1162                assert_approx_equal_symmetry_test!(rate_in, solution.rate());
1163            }
1164            if solution.calculated_field().is_periods() {
1165                // There are a few special cases in which the number of periods might be zero or one
1166                // instead of matching periods_in. So check against the number returned from
1167                // periods().
1168                assert_eq!(periods_calc, solution.periods());
1169            } else {
1170                assert_eq!(periods_in, solution.periods());
1171            }
1172            assert_approx_equal_symmetry_test!(present_value_in, solution.present_value());
1173            assert_approx_equal_symmetry_test!(future_value_calc, solution.future_value());
1174        }
1175
1176        let mut schedules = vec![future_value_schedule_solution(&rates_in, present_value_in)];
1177        if future_value_calc.is_normal() {
1178            schedules.push(present_value_schedule_solution(&rates_in, future_value_calc));
1179        }
1180
1181        for schedule in schedules.iter() {
1182            //bg!(schedule);
1183            assert_eq!(periods_in, schedule.rates().len() as u32);
1184            assert_eq!(periods_in, schedule.periods());
1185            assert_approx_equal_symmetry_test!(present_value_in, schedule.present_value());
1186            assert_approx_equal_symmetry_test!(future_value_calc, schedule.future_value());
1187        }
1188
1189        // Check each series in isolation.
1190        for solution in solutions.iter() {
1191            let label = format!("Solution for {:?}", solution.calculated_field());
1192            //bg!(&label);
1193            check_series_internal(label, solution.calculated_field(), &solution.series(), rate_in, periods_in, present_value_in, future_value_calc, rate_calc, periods_calc);
1194        }
1195        for solution in schedules.iter() {
1196            let label = format!("Schedule for {:?}", solution.calculated_field());
1197            //bg!(&label);
1198            check_series_internal(label, solution.calculated_field(),  &solution.series(), rate_in, periods_in, present_value_in, future_value_calc, rate_calc, periods_calc);
1199        }
1200
1201        // Confirm that all of the series have the same values for all periods regardless of how we
1202        // did the calculation. For the reference solution take the result of
1203        // future_value_solution(). It would also work to use the result of rate_solution() and
1204        // present_value_solution() but not periods_solution() since there are some special cases in
1205        // which this will create fewer periods than the other functions.
1206        let reference_solution = solutions.iter().find(|solution| solution.calculated_field().is_future_value()).unwrap();
1207        let reference_series = reference_solution.series();
1208        for solution in solutions.iter().filter(|solution| !solution.calculated_field().is_future_value()) {
1209            let label = format!("Solution for {:?}", solution.calculated_field());
1210            check_series_same_values(reference_solution, &reference_series,label, solution.calculated_field(), &solution.series());
1211        }
1212        for schedule in schedules.iter() {
1213            let label = format!("Schedule for {:?}", schedule.calculated_field());
1214            check_series_same_values(reference_solution, &reference_series, label, schedule.calculated_field(), &schedule.series());
1215        }
1216    }
1217
1218    fn check_series_internal(_label: String, calculated_field: &TvmVariable, series: &TvmSeries, rate_in: f64, periods_in: u32, present_value_in: f64, future_value_calc: f64, rate_calc: f64, periods_calc: u32) {
1219        //bg!(label);
1220        //bg!(&series);
1221        if calculated_field.is_periods() {
1222            // There are a few special cases in which the number of periods might be zero or one
1223            // instead of matching periods_in. So check against the number returned from
1224            // periods().
1225            assert_eq!(periods_calc + 1, series.len() as u32);
1226        } else {
1227            assert_eq!(periods_in + 1, series.len() as u32);
1228        }
1229        let mut prev_value: Option<f64> = None;
1230        for (period, entry) in series.iter().enumerate() {
1231            assert_eq!(period as u32, entry.period());
1232            if period == 0 {
1233                assert_approx_equal_symmetry_test!(0.0, entry.rate());
1234                // The first entry should always contain the starting value.
1235                assert_approx_equal_symmetry_test!(-present_value_in, entry.value());
1236            } else {
1237                // We're past period 0.
1238                let effective_rate = if calculated_field.is_rate() {
1239                    // There are a few special cases in which the calculated rate is arbitrarily set
1240                    // to zero since any value would work. We've already checked rate_calc against
1241                    // those special cases, so use that here for the comparison.
1242                    assert_approx_equal_symmetry_test!(rate_calc, entry.rate());
1243                    rate_calc
1244                } else {
1245                    assert_approx_equal_symmetry_test!(rate_in, entry.rate());
1246                    rate_in
1247                };
1248                // Compare this period's value to the one before.
1249                if is_approx_equal!(0.0, effective_rate) || is_approx_equal!(0.0, prev_value.unwrap()) {
1250                    // The rate is zero or the previous value was zero so each period's value should
1251                    // be the same as the one before.
1252                    assert_approx_equal_symmetry_test!(entry.value(), prev_value.unwrap());
1253                } else if effective_rate < 0.0 {
1254                    // The rate is negative so the value should be shrinking from period to period,
1255                    // but since the value could be negative shrinking in this case means getting
1256                    // closer to zero.
1257                    assert!(entry.value.abs() < prev_value.unwrap().abs());
1258                } else {
1259                    // The rate is negative so the value should be growing from period to period,
1260                    // but since the value could be negative growing in this case means moving away
1261                    // from zero.
1262                    assert!(entry.value.abs() > prev_value.unwrap().abs());
1263                }
1264                /*
1265                } else if present_value_in.signum() == effective_rate.signum() {
1266                    // Either the starting value and the rate are both positive or they're both
1267                    // negative. In either case each period's value should be greater than the one
1268                    // before.
1269                    assert!(entry.value() > prev_value.unwrap());
1270                } else {
1271                    // Either the starting value is positive and the rate is negative or vice versa.
1272                    // In either case each period's value should be smaller than the one before.
1273                    assert!(entry.value() < prev_value.unwrap());
1274                }*/
1275            }
1276            if period == series.len() - 1 {
1277                // This is the last period's entry. It should contain the future value.
1278                //bg!(future_value_calc, entry.value());
1279                assert_approx_equal_symmetry_test!(future_value_calc, entry.value());
1280            }
1281            prev_value = Some(entry.value());
1282        }
1283    }
1284
1285    fn check_series_same_values(_reference_solution: &TvmSolution, reference_series: &TvmSeries, _label: String, calculated_field: &TvmVariable, series: &[TvmPeriod]) {
1286        //bg!(reference_solution);
1287        //bg!(&reference_series);
1288
1289        //bg!(label);
1290        //bg!(&series);
1291
1292        if calculated_field.is_periods() && reference_series.len() != series.len() {
1293
1294            // There are a few special cases in which the number of periods might be zero or one
1295            // instead of matching periods_in.
1296
1297            // There will always be at least a period 0.
1298            let reference_entry = &reference_series[0];
1299            let entry = &series[0];
1300            //bg!(&reference_entry, &entry);
1301            assert_eq!(reference_entry.period(), entry.period());
1302            assert_approx_equal_symmetry_test!(reference_entry.rate(), entry.rate());
1303            assert_approx_equal_symmetry_test!(reference_entry.value(), entry.value());
1304
1305            // Check the last period.
1306            let reference_entry = &reference_series.last().unwrap();
1307            let entry = &series.last().unwrap();
1308            //bg!(&reference_entry, &entry);
1309            if reference_series.len() > 1 && series.len() > 1 {
1310                assert_approx_equal_symmetry_test!(reference_entry.rate(), entry.rate());
1311            }
1312            assert_approx_equal_symmetry_test!(reference_entry.value(), entry.value());
1313        } else {
1314
1315            // This is the usual case where we expect the two series to be identical except for
1316            // the formulas.
1317
1318            assert_eq!(reference_series.len(), series.len());
1319
1320            for (period, reference_entry) in reference_series.iter().enumerate() {
1321                let entry = &series[period];
1322                //bg!(&reference_entry, &entry);
1323                assert_eq!(reference_entry.period(), entry.period());
1324                if calculated_field.is_rate() {
1325                    // There are a few special cases where the calculated rate will be zero since
1326                    // any answer would work.
1327                    if entry.rate() != 0.0 {
1328                        assert_approx_equal_symmetry_test!(reference_entry.rate(), entry.rate());
1329                    }
1330                } else {
1331                    assert_approx_equal_symmetry_test!(reference_entry.rate(), entry.rate());
1332                }
1333                //bg!(reference_entry.value(), round_4(reference_entry.value()), entry.value(), round_4(entry.value()));
1334                assert_approx_equal_symmetry_test!(reference_entry.value(), entry.value());
1335                // assert_eq!(reference_entry.value.round(), entry.value.round());
1336            }
1337        }
1338    }
1339
1340    #[test]
1341    fn test_continuous_symmetry_one() {
1342        let rate = 0.10;
1343        let periods = 4;
1344        let present_value = 5_000.00;
1345        check_continuous_symmetry(rate, periods, present_value);
1346    }
1347
1348    /*
1349    #[test]
1350    fn test_symmetry_multiple() {
1351        let rates = vec![-1.0, -0.5, -0.05, -0.005, 0.0, 0.005, 0.05, 0.5, 1.0, 10.0, 100.0];
1352        // let rates = vec![-0.5, -0.05, -0.005, 0.0, 0.005, 0.05, 0.5, 1.0, 10.0, 100.0];
1353        // let periods: Vec<u32> = vec![0, 1, 2, 5, 10, 36, 100, 1_000];
1354        let periods: Vec<u32> = vec![0, 1, 2, 5, 10, 36];
1355        let present_values: Vec<f64> = vec![-1_000_000.0, -1_234.98, -1.0, 0.0, 5.55555, 99_999.99];
1356        for rate_one in rates.iter() {
1357            for periods_one in periods.iter() {
1358                for present_value_one in present_values.iter() {
1359                    if !(*periods_one > 50 && *rate_one > 0.01) {
1360                        check_symmetry(*rate_one, *periods_one, *present_value_one);
1361                    }
1362                }
1363            }
1364        }
1365    }
1366    */
1367
1368    fn check_continuous_symmetry(rate_in: f64, periods_in: u32, present_value_in: f64) {
1369        let display = false;
1370
1371        if display {
1372            println!();
1373            dbg!("check_continuous_symmetry", rate_in, periods_in, present_value_in);
1374        }
1375
1376        /*
1377        let fv_calc = present_value_in * std::f64::consts::E.powf(rate_in * periods_in as f64);
1378        dbg!(fv_calc);
1379        let pv_calc = fv_calc / std::f64::consts::E.powf(rate_in * periods_in as f64);
1380        dbg!(pv_calc);
1381        */
1382
1383        // Calculate the future value given the other three inputs so that we have all four values
1384        // which we can use in various combinations to confirm that all four continuous TVM
1385        // functions return consistent values.
1386        let future_value_calc = future_value(rate_in, periods_in, present_value_in, true);
1387        if display { dbg!(future_value_calc); }
1388
1389        let rate_calc = rate::rate(periods_in, present_value_in, future_value_calc, true);
1390        if display { dbg!(rate_calc); }
1391        if periods_in == 0 || present_value_in == 0.0 {
1392            // With zero periods or zero for the present value, presumably the future value is the
1393            // same as the present value and any rate would be fine so we arbitrarily
1394            // return zero.
1395            assert_approx_equal_symmetry_test!(present_value_in, future_value_calc);
1396            assert_approx_equal_symmetry_test!(0.0, rate_calc);
1397        } else {
1398            if display { dbg!(rate_calc, rate_in); }
1399            assert_approx_equal_symmetry_test!(rate_calc, rate_in);
1400        }
1401
1402        let fractional_periods_calc = periods(rate_in, present_value_in, future_value_calc, true);
1403        if display { dbg!(fractional_periods_calc); }
1404        let periods_calc = round_4(fractional_periods_calc).ceil() as u32;
1405        if display { dbg!(periods_calc); }
1406        if rate_in == 0.0 || present_value_in == 0.0 || periods_in == 0 {
1407            // If the rate is zero or the present value is zero then the present value and future
1408            // value will be the same and periods() will return zero since no periods are required.
1409            assert_approx_equal_symmetry_test!(present_value_in, future_value_calc);
1410            assert_eq!(0, periods_calc);
1411        } else if rate_in == -1.0 {
1412            // The investment will drop to zero by the end of the first period so periods() will
1413            // return 1.
1414            assert_approx_equal_symmetry_test!(0.0, future_value_calc);
1415            assert_eq!(1, periods_calc);
1416        } else {
1417            // This is the normal case and we expect periods() to return the same number of periods
1418            // we started with.
1419            assert_eq!(periods_calc, periods_in);
1420        }
1421
1422        if future_value_calc.is_normal() {
1423            let present_value_calc = present_value(rate_in, periods_in, future_value_calc, true);
1424            if display { dbg!(present_value_calc); }
1425            assert_approx_equal_symmetry_test!(present_value_calc, present_value_in);
1426        };
1427
1428        // Create TvmSolution structs by solving for each of the four possible variables.
1429        let mut solutions = vec![
1430            rate_solution(periods_in, present_value_in, future_value_calc, true),
1431            periods_solution(rate_in, present_value_in, future_value_calc, true),
1432            future_value_solution(rate_in, periods_in, present_value_in, true),
1433        ];
1434
1435        if future_value_calc.is_normal() {
1436            solutions.push(present_value_solution(rate_in, periods_in, future_value_calc, true));
1437        }
1438        for solution in solutions.iter() {
1439            if display { dbg!(solution); }
1440            // let series = solution.series();
1441            // dbg!(&series);
1442            if solution.calculated_field().is_rate() {
1443                // There are a few special cases in which the calculated rate is arbitrarily set to
1444                // zero since any value would work. We've already checked rate_calc against those
1445                // special cases, so use that here for the comparison.
1446                assert_approx_equal_symmetry_test!(rate_calc, solution.rate());
1447            } else {
1448                assert_approx_equal_symmetry_test!(rate_in, solution.rate());
1449            }
1450            if solution.calculated_field().is_periods() {
1451                // There are a few special cases in which the number of periods might be zero or one
1452                // instead of matching periods_in. So check against the number returned from
1453                // periods().
1454                assert_eq!(periods_calc, solution.periods());
1455            } else {
1456                assert_eq!(periods_in, solution.periods());
1457            }
1458            assert_approx_equal_symmetry_test!(present_value_in, solution.present_value());
1459            assert_approx_equal_symmetry_test!(future_value_calc, solution.future_value());
1460        }
1461
1462        // Check each series in isolation.
1463        /*
1464        for solution in solutions.iter() {
1465            let label = format!("Solution for {:?}", solution.calculated_field());
1466            //bg!(&label);
1467            check_series_internal(label, solution.calculated_field().clone(), &solution.series(), rate_in, periods_in, present_value_in, future_value_calc, rate_calc, periods_calc);
1468        }
1469        */
1470
1471        // Confirm that all of the series have the same values for all periods regardless of how we
1472        // did the calculation. For the reference solution take the result of
1473        // future_value_solution(). It would also work to use the result of rate_solution() and
1474        // present_value_solution() but not periods_solution() since there are some special cases in
1475        // which this will create fewer periods than the other functions.
1476        let reference_solution = solutions.iter().find(|solution| solution.calculated_field().is_future_value()).unwrap();
1477        let reference_series = reference_solution.series();
1478        for solution in solutions.iter().filter(|solution| !solution.calculated_field().is_future_value()) {
1479            let label = format!("Solution for {:?}", solution.calculated_field());
1480            check_series_same_values(reference_solution, &reference_series, label, solution.calculated_field(), &solution.series());
1481        }
1482    }
1483
1484    #[test]
1485    fn test_simple_to_continuous_symmetry_one() {
1486        let rate = 0.10;
1487        let periods = 4;
1488        let present_value = 5_000.00;
1489        check_simple_to_continuous_symmetry(rate, periods, present_value);
1490    }
1491
1492    /*
1493    #[test]
1494    fn test_symmetry_multiple() {
1495        let rates = vec![-1.0, -0.5, -0.05, -0.005, 0.0, 0.005, 0.05, 0.5, 1.0, 10.0, 100.0];
1496        // let rates = vec![-0.5, -0.05, -0.005, 0.0, 0.005, 0.05, 0.5, 1.0, 10.0, 100.0];
1497        // let periods: Vec<u32> = vec![0, 1, 2, 5, 10, 36, 100, 1_000];
1498        let periods: Vec<u32> = vec![0, 1, 2, 5, 10, 36];
1499        let present_values: Vec<f64> = vec![-1_000_000.0, -1_234.98, -1.0, 0.0, 5.55555, 99_999.99];
1500        for rate_one in rates.iter() {
1501            for periods_one in periods.iter() {
1502                for present_value_one in present_values.iter() {
1503                    if !(*periods_one > 50 && *rate_one > 0.01) {
1504                        check_symmetry(*rate_one, *periods_one, *present_value_one);
1505                    }
1506                }
1507            }
1508        }
1509    }
1510    */
1511
1512    fn check_simple_to_continuous_symmetry(rate_in: f64, periods_in: u32, present_value_in: f64) {
1513        println!();
1514        dbg!("check_simple_to_continuous_symmetry", rate_in, periods_in, present_value_in);
1515
1516        // Calculate the future value given the other three inputs so that we have all four values
1517        // which we can use in various combinations to confirm that all four continuous TVM
1518        // functions return consistent values.
1519        let future_value_calc = future_value(rate_in, periods_in, present_value_in, true);
1520        dbg!(future_value_calc);
1521
1522        // Create TvmSolution structs with continuous compounding by solving for each of the four possible variables.
1523        let continuous_solutions = vec![
1524            rate_solution(periods_in, present_value_in, future_value_calc, true),
1525            periods_solution(rate_in, present_value_in, future_value_calc, true),
1526            present_value_solution(rate_in, periods_in, future_value_calc, true),
1527            future_value_solution(rate_in, periods_in, present_value_in, true),
1528        ];
1529
1530        // For each solution with continuous compounding create a corresponding solution with
1531        // simple compounding.
1532        /*
1533        let simple_solutions = continuous_solutions.iter()
1534            .map(|continuous_solution| continuous_solution.with_simple_compounding())
1535            .collect::<Vec<_>>();
1536        */
1537        let simple_solutions = [
1538            continuous_solutions[0].rate_solution(false, None),
1539            continuous_solutions[1].periods_solution(false),
1540            continuous_solutions[2].present_value_solution(false, None),
1541            continuous_solutions[3].future_value_solution(false, None),
1542        ];
1543
1544        // Compare the continuous solutions to the corresponding simple solutions.
1545        for (index, continuous_solution) in continuous_solutions.iter().enumerate() {
1546            let simple_solution = &simple_solutions[index];
1547            println!("\nContinuous compounding vs. simple compounding adjusting {} while keeping the other three values constant.\n", continuous_solution.calculated_field().to_string().to_lowercase());
1548            dbg!(&continuous_solution, &simple_solution);
1549            assert_eq!(continuous_solution.calculated_field(), simple_solution.calculated_field());
1550            assert!(continuous_solution.continuous_compounding());
1551            assert!(!simple_solution.continuous_compounding());
1552            if continuous_solution.calculated_field().is_rate() {
1553                // We expect the rate to be lower with continuous compounding when the other three
1554                // inputs are held constant.
1555                assert!(continuous_solution.rate().abs() < simple_solution.rate().abs());
1556            } else {
1557                // The rate was an input rather than being calculated, so it should be the same.
1558                assert_eq!(continuous_solution.rate(), simple_solution.rate());
1559            }
1560            if continuous_solution.calculated_field().is_periods() {
1561                // We expect the fractional periods to be the same or lower with continuous
1562                // compounding when the other three inputs are held constant.
1563                assert!(continuous_solution.fractional_periods() <= simple_solution.fractional_periods());
1564                // Depending on rounding the number of periods may be the same or less for
1565                // continuous compounding.
1566                assert!(continuous_solution.periods() <= simple_solution.periods());
1567            } else {
1568                // The number of periods was an input rather than being calculated, so it should be
1569                // the same.
1570                assert_eq!(continuous_solution.periods(), simple_solution.periods());
1571            }
1572            if continuous_solution.calculated_field().is_present_value() {
1573                // We expect the present value to be lower with continuous compounding when the
1574                // other three inputs are held constant. This is because it takes less of an initial
1575                // investment to reach the same final value.
1576                assert!(continuous_solution.present_value().abs() < simple_solution.present_value().abs());
1577            } else {
1578                // The present value was an input rather than being calculated, so it should be the
1579                // same.
1580                assert_eq!(continuous_solution.present_value(), simple_solution.present_value());
1581            }
1582            if continuous_solution.calculated_field().is_future_value() {
1583                // We expect the future value to be higher with continuous compounding when the
1584                // other three inputs are held constant.
1585                assert!(continuous_solution.future_value().abs() > simple_solution.future_value().abs());
1586            } else {
1587                // The future value was an input rather than being calculated, so it should be the
1588                // same.
1589                assert_eq!(continuous_solution.future_value(), simple_solution.future_value());
1590            }
1591            assert_ne!(continuous_solution.formula(), simple_solution.formula());
1592            assert_ne!(continuous_solution.symbolic_formula(), simple_solution.symbolic_formula());
1593        }
1594
1595        // For each solution with simple compounding create a corresponding solution with
1596        // continuous compounding. This should get us back to the equivalents of our original list
1597        // of solutions with continuous compounding.
1598        /*
1599        let continuous_solutions_round_trip = simple_solutions.iter()
1600            .map(|simple_solution| simple_solution.with_continuous_compounding())
1601            .collect::<Vec<_>>();
1602        */
1603        let continuous_solutions_round_trip = [
1604            continuous_solutions[0].rate_solution(true, None),
1605            continuous_solutions[1].periods_solution(true),
1606            continuous_solutions[2].present_value_solution(true, None),
1607            continuous_solutions[3].future_value_solution(true, None),
1608        ];
1609
1610        // Compare the recently created continuous solutions to the original continuous solutions.
1611        for (index, solution) in continuous_solutions.iter().enumerate() {
1612            let solution_round_trip = &continuous_solutions_round_trip[index];
1613            println!("\nOriginal continuous compounding vs. derived continuous compounding where the calculated field is {}.\n", solution.calculated_field().to_string().to_lowercase());
1614            dbg!(&solution, &solution_round_trip);
1615            assert_eq!(solution, solution_round_trip);
1616        }
1617        /*
1618        for (calculated_field, continuous_solution) in continuous_solutions.iter() {
1619            dbg!(&continuous_solution);
1620            dbg!(&continuous_solution.series());
1621
1622        }
1623        */
1624
1625        // Check each series in isolation.
1626        /*
1627        for solution in solutions.iter() {
1628            let label = format!("Solution for {:?}", solution.calculated_field());
1629            //bg!(&label);
1630            check_series_internal(label, solution.calculated_field().clone(), &solution.series(), rate_in, periods_in, present_value_in, future_value_calc, rate_calc, periods_calc);
1631        }
1632        */
1633
1634        /*
1635        // Confirm that all of the series have the same values for all periods regardless of how we
1636        // did the calculation. For the reference solution take the result of
1637        // future_value_solution(). It would also work to use the result of rate_solution() and
1638        // present_value_solution() but not periods_solution() since there are some special cases in
1639        // which this will create fewer periods than the other functions.
1640        let reference_solution = solutions.iter().find(|x| x.calculated_field().is_future_value()).unwrap();
1641        for solution in solutions.iter().filter(|x| !x.calculated_field().is_future_value()) {
1642            let label = format!("Solution for {:?}", solution.calculated_field());
1643            check_series_same_values(reference_solution, label, solution.calculated_field().clone(), &solution.series());
1644        }
1645        */
1646    }
1647
1648    fn setup_for_compounding_periods() -> (TvmSolution, Vec<u32>) {
1649        let rate = 0.10;
1650        let periods = 4;
1651        let present_value = 5_000.00;
1652        let compounding_periods = vec![1, 2, 4, 6, 12, 24, 52, 365];
1653        (future_value_solution(rate, periods, present_value, false), compounding_periods)
1654    }
1655
1656    #[test]
1657    fn test_with_compounding_periods_vary_future_value() {
1658        println!("\ntest_with_compounding_periods_vary_future_value()\n");
1659
1660        let (solution, compounding_periods) = setup_for_compounding_periods();
1661        dbg!(&compounding_periods);
1662
1663        for one_compounding_period in compounding_periods.iter() {
1664            println!("\nSimple compounding original vs. compounding periods = {} while varying future value.\n", one_compounding_period);
1665            dbg!(&solution, solution.future_value_solution(false, Some(*one_compounding_period)));
1666        }
1667    }
1668
1669    #[test]
1670    fn test_with_compounding_periods_vary_present_value() {
1671        println!("\ntest_with_compounding_periods_vary_present_value()\n");
1672
1673        let (solution, compounding_periods) = setup_for_compounding_periods();
1674        dbg!(&compounding_periods);
1675
1676        for one_compounding_period in compounding_periods.iter() {
1677            println!("\nSimple compounding original vs. compounding periods = {} while varying present value.\n", one_compounding_period);
1678            dbg!(&solution, solution.present_value_solution(false, Some(*one_compounding_period)));
1679        }
1680    }
1681}