Skip to main content

finance_solution/tvm/
mod.rs

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