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