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}