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