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