finance_solution/cashflow/net_present_value.rs
1//! **Net Present Value calculations**. Given cashflows (including the time-0 investment), periods,
2//! and fixed or varying discount rates, what is the net value of the series right now?
3//!
4//! Prefer [`net_present_value_schedule_solution`] when rates or cashflows vary — it carries period
5//! detail and pretty tables. For a constant rate and constant periodic cashflow, use
6//! [`net_present_value`] / [`net_present_value_solution`].
7//!
8//! ## Schedule input shapes
9//!
10//! [`net_present_value_schedule`] accepts **rates** and **cashflows** with flexible lengths.
11//! Cashflows always include the **time-0 investment** as `cashflows[0]` (negative or zero).
12//! Rates apply to the *forward* periods (not time 0).
13//!
14//! | Shape | `rates` length | `cashflows` length | Meaning |
15//! |-------|----------------|--------------------|---------|
16//! | **A. Single period** | 1 | 2 (`[t0, t1]`) | One discount rate, one future cashflow |
17//! | **B. Full series** | N | N+1 | Rate per period 1..=N; cashflow per t0..=tN |
18//! | **C. Repeating cashflow** | N | 2 (`[t0, c]`) | Rate series; cashflow `c` repeats each period |
19//! | **D. Repeating rate** | 1 | N+1 | One rate broadcast to every period; full cashflows |
20//!
21//! Invalid combinations (both sides incomplete, empty slices, positive t0 investment,
22//! length mismatch on full series) return [`FinanceError`](crate::FinanceError).
23//!
24//! ### Shape A — single period
25//! ```
26//! use finance_solution::net_present_value_schedule;
27//! let npv = net_present_value_schedule(&[0.05], &[-1_000.0, 1_100.0]).unwrap();
28//! assert!((npv - 47.6190476).abs() < 1e-6);
29//! ```
30//!
31//! ### Shape B — full varying rates and cashflows
32//! ```
33//! use finance_solution::net_present_value_schedule;
34//! let rates = [0.034, 0.089, 0.055];
35//! let cashflows = [-1_000.0, 200.0, 300.0, 500.0];
36//! let npv = net_present_value_schedule(&rates, &cashflows).unwrap();
37//! assert!((npv - (-127.8016238)).abs() < 1e-6);
38//! ```
39//!
40//! ### Shape C — rate series, constant periodic cashflow
41//! ```
42//! use finance_solution::net_present_value_schedule;
43//! // rates for 3 periods; cashflows = [investment, repeating payment]
44//! let npv = net_present_value_schedule(&[0.03, 0.04, 0.05], &[-1_000.0, 400.0]).unwrap();
45//! assert!(npv.is_finite());
46//! ```
47//!
48//! ### Shape D — constant rate, full cashflow ladder
49//! ```
50//! use finance_solution::net_present_value_schedule;
51//! let npv = net_present_value_schedule(&[0.034], &[-1_000.0, 300.0, 400.0, 500.0]).unwrap();
52//! assert!(npv.is_finite());
53//! ```
54//!
55//! ### Constant rate + constant cashflow (non-schedule API)
56//! ```
57//! use finance_solution::net_present_value_solution;
58//! let (rate, periods, initial_investment, cashflow) = (0.034, 3, -1000, 400);
59//! let npv = net_present_value_solution(rate, periods, initial_investment, cashflow).unwrap();
60//! let _ = npv; // .print_table() in apps
61//! ```
62//! Sample table:
63//! ```text
64//! period rate present_value future_value investment_value
65//! ------ ------ ------------- ------------ ----------------
66//! 0 0.0000 -1_000.0000 -1_000.0000 -1_000.0000
67//! 1 0.0340 386.8472 400.0000 -613.1528
68//! 2 0.0340 374.1269 400.0000 -239.0259
69//! 3 0.0340 361.8248 400.0000 122.7989
70//! ```
71//!
72//! ### Schedule solution with table
73//! ```
74//! use finance_solution::net_present_value_schedule_solution;
75//! let rates = vec![0.034, 0.034, 0.034];
76//! let cashflows = vec![-1000, 300, 400, 500];
77//! let npv = net_present_value_schedule_solution(&rates, &cashflows).unwrap();
78//! let _ = npv; // .print_table() in apps
79//! ```
80
81// use crate::cashflow::*;
82// Needed for the Rustdoc comments.
83#[allow(unused_imports)]
84use crate::present_value_annuity::present_value_annuity;
85use crate::*;
86
87use std::ops::Deref;
88
89/// Returns the net present value of a future series of constant cashflows and constant rate, subtracting the initial investment cost. Returns f64.
90///
91/// Related functions:
92/// * To calculate a net present value with a varying rate or varying cashflow or both, use [`net_present_value_schedule`].
93///
94/// The net present value annuity formula is:
95///
96/// npv = initial_investment + sum( cashflow / (1 + rate)<sup>period</sup> )
97///
98/// or
99///
100/// npv = initial_investment + cashflow * ((1. - (1. / (1. + rate)).powf(periods)) / rate)
101///
102/// # Arguments
103/// * `rate` - The rate at which the investment grows or shrinks per period,
104/// expressed as a floating point number. For instance 0.05 would mean 5%. Often appears as
105/// `r` or `i` in formulas.
106/// * `periods` - The number of periods such as quarters or years. Often appears as `n` or `t`.
107/// * `cashflow` - The value of the constant cashflow (aka payment).
108/// * `initial investment` - The value of the initial investment (should be negative, or 0).
109///
110/// # Errors
111/// Returns [`crate::FinanceError`] if `initial_investment` is positive. This value should always be negative, and cashflows be positive, or the reverse, because these monies are going opposite directions.
112///
113/// # Examples
114/// Net Present Value of a series of -$1000 investment which will payback $500 yearly for 10 years.
115/// ```
116/// use finance_solution::*;
117/// let (rate, periods, initial_investment, cashflow) = (0.034, 10, -1000, 500);
118///
119/// // Find the present value of this scenario.
120/// let net_present_value = net_present_value(rate, periods, initial_investment, cashflow).unwrap();
121///
122/// // Confirm that the present value is correct to four decimal places (one hundredth of a cent).
123/// assert_approx_equal!(3179.3410288, net_present_value);
124/// ```
125pub fn net_present_value<C, I>(
126 rate: f64,
127 periods: u32,
128 initial_investment: I,
129 cashflow: C,
130) -> crate::FinanceResult<f64>
131where
132 I: Into<f64> + Copy,
133 C: Into<f64> + Copy,
134{
135 let annuity = cashflow.into();
136 let ii = initial_investment.into();
137 crate::util::error::require_rate_gt_minus_one(rate)?;
138 crate::util::error::require_money("initial_investment", ii)?;
139 crate::util::error::require_money("cashflow", annuity)?;
140 if periods == 0 {
141 return Ok(ii);
142 }
143 let pv_cashflow = if rate == 0.0 {
144 annuity * periods as f64
145 } else {
146 annuity * ((1.0 - (1.0 / (1.0 + rate)).powf(periods as f64)) / rate)
147 };
148 let npv = ii + pv_cashflow;
149 if npv.is_finite() {
150 Ok(npv)
151 } else {
152 Err(crate::FinanceError::NonFinite {
153 field: "net_present_value",
154 value: npv,
155 })
156 }
157}
158
159/// Returns the net present value of a future series of constant cashflows and constant rate, subtracting the initial investment cost. Returns a solution struct with additional features..
160///
161/// Related functions:
162/// * To calculate a net present value with a varying rate or varying cashflow or both, use [`net_present_value_schedule`].
163///
164/// The net present value annuity formula is:
165///
166/// npv = initial_investment + sum( cashflow / (1 + rate)<sup>period</sup> )
167///
168/// or
169///
170/// npv = initial_investment + cashflow * ((1. - (1. / (1. + rate)).powf(periods)) / rate)
171///
172/// # Arguments
173/// * `rate` - The rate at which the investment grows or shrinks per period,
174/// expressed as a floating point number. For instance 0.05 would mean 5%. Often appears as
175/// `r` or `i` in formulas.
176/// * `periods` - The number of periods such as quarters or years. Often appears as `n` or `t`.
177/// * `cashflow` - The value of the constant cashflow (aka payment).
178/// * `initial investment` - The value of the initial investment (should be negative, or 0).
179pub fn net_present_value_solution<C, I>(
180 rate: f64,
181 periods: u32,
182 initial_investment: I,
183 cashflow: C,
184) -> crate::FinanceResult<NpvSolution>
185where
186 I: Into<f64> + Copy,
187 C: Into<f64> + Copy,
188{
189 let annuity = cashflow.into();
190 let ii = initial_investment.into();
191 crate::util::error::require_rate_gt_minus_one(rate)?;
192 crate::util::error::require_money("initial_investment", ii)?;
193 crate::util::error::require_money("cashflow", annuity)?;
194 let rates = repeating_vec![rate, periods];
195 let mut cashflows = repeating_vec![annuity, periods];
196 cashflows.insert(0, ii);
197 net_present_value_schedule_solution(&rates, &cashflows)
198}
199
200/// Returns the net present value of a schedule of rates and cashflows (can be varying), subtracting the initial investment cost. Returns f64.
201///
202/// # Examples
203/// Net Present Value of a series of -$1000 investment which will payback $500 yearly for 10 years.
204/// ```
205/// use finance_solution::*;
206/// let (rates, cashflows) = (vec![0.034, 0.089, 0.055], vec![-1000, 200, 300, 500]);
207///
208/// // Find the present value of this scenario.
209/// let net_present_value = net_present_value_schedule(&rates, &cashflows).unwrap();
210///
211/// // Confirm that the present value is correct to four decimal places (one hundredth of a cent).
212/// assert_approx_equal!(-127.8016238, net_present_value);
213///
214/// // present_value(0.034, 1, 200): $193.42
215/// // present_value(0.089, 2, 300): $252.97
216/// // present_value(0.055, 3, 500): $425.81
217/// // initial investment: -$1000
218/// // sum of the above: -$127.80 (net present value)
219///
220/// ```
221pub fn net_present_value_schedule<C>(rates: &[f64], cashflows: &[C]) -> crate::FinanceResult<f64>
222where
223 C: Into<f64> + Copy,
224{
225 let (periods, r, c, initial_investment) = check_schedule(rates, cashflows)?;
226 // let mut cflows = vec![];
227 // for i in 0..cashflows.len() {
228 // cflows.push(cashflows[i].into());
229 // }
230 // let cashflows = &cflows;
231 // assert!(cashflows[0] <= 0.0, "The initial investment (cashflows[0]) should be negative or zero");
232 // assert!(cashflows.len() >= 2, "Must provide at least 2 values in cashflows, the initial investment at the 0 position and the following cashflows, or a single cashflow representing a repeating constant cashflow.");
233 // assert!(rates.len() >= 1, "Must provide at least 1 rate.");
234 // let rate_length = rates.len();
235 // let cashflow_length = cashflows.len();
236 // let initial_investment = cashflows[0];
237 // let mut cashflow_vec = vec![initial_investment];
238 // let mut rate_vec = vec![];
239 // let periods: u32;
240 // let r: &[f64];
241 // let c: &[f64];
242
243 // if rate_length == 1 && cashflow_length == 2 {
244 // r = &rates;
245 // c = &cashflows;
246 // periods = 1_u32;
247 // } else if rate_length > 1 && cashflow_length > 2 {
248 // r = &rates;
249 // c = &cashflows;
250 // periods = rate_length as u32;
251 // } else if rate_length > 1 && cashflow_length == 2 {
252 // r = &rates;
253 // periods = rate_length as u32;
254 // for _i in 0..periods {
255 // cashflow_vec.push(cashflows[1])
256 // }
257 // c = &cashflow_vec;
258 // } else if rate_length == 1 && cashflow_length > 2 {
259 // c = &cashflows;
260 // periods = cashflow_length as u32 - 1;
261 // for _i in 0..periods {
262 // rate_vec.push(rates[0])
263 // }
264 // r = &rate_vec;
265 // } else {
266 // // revise this panic message
267 // panic!("At least rates or cashflows for net_present_value_schedule must provide the full series of inputs. Only one input can be a shorthand expression of a repeating input. If both are repeating constant inputs, use the net_present_value function.");
268 // }
269
270 let mut pv_accumulator = 0_f64;
271 for i in 0..periods {
272 let present_value =
273 -present_value(r[i as usize], (i + 1) as u32, c[i as usize + 1], false)?;
274 pv_accumulator += present_value;
275 }
276 let npv = initial_investment + pv_accumulator;
277 if npv.is_finite() {
278 Ok(npv)
279 } else {
280 Err(crate::FinanceError::NonFinite {
281 field: "net_present_value",
282 value: npv,
283 })
284 }
285}
286
287fn check_schedule<C>(
288 rates: &[f64],
289 cashflows: &[C],
290) -> crate::FinanceResult<(u32, Vec<f64>, Vec<f64>, f64)>
291where
292 C: Into<f64> + Copy,
293{
294 let mut cflows = vec![];
295 for i in 0..cashflows.len() {
296 cflows.push(cashflows[i].into());
297 }
298 let cashflows = &cflows;
299 if cashflows.is_empty() {
300 return Err(crate::FinanceError::EmptyInput { what: "cashflows" });
301 }
302 if cashflows[0] > 0.0 {
303 return Err(crate::FinanceError::InvalidCashflow {
304 message: "initial investment (cashflows[0]) should be negative or zero",
305 });
306 }
307 if cashflows.len() < 2 {
308 return Err(crate::FinanceError::InvalidCashflow {
309 message: "must provide at least 2 cashflows: initial investment and one cashflow",
310 });
311 }
312 if rates.is_empty() {
313 return Err(crate::FinanceError::EmptyInput { what: "rates" });
314 }
315 crate::util::error::require_rates(rates)?;
316 for (i, &cf) in cashflows.iter().enumerate() {
317 crate::util::error::require_money("cashflow", cf).map_err(|_| {
318 crate::FinanceError::NonFinite {
319 field: "cashflow",
320 value: cf,
321 }
322 })?;
323 let _ = i;
324 }
325 let rate_length = rates.len();
326 let cashflow_length = cashflows.len();
327 let initial_investment = cashflows[0];
328 let mut cashflow_vec = vec![initial_investment];
329 let mut rate_vec = vec![];
330 let periods: u32;
331 let r: &[f64];
332 let c: &[f64];
333
334 if rate_length == 1 && cashflow_length == 2 {
335 r = rates;
336 c = cashflows;
337 periods = 1_u32;
338 } else if rate_length > 1 && cashflow_length > 2 {
339 if rate_length != cashflow_length - 1 {
340 return Err(crate::FinanceError::LengthMismatch {
341 left: rate_length,
342 right: cashflow_length - 1,
343 context: "npv rates vs cashflow periods",
344 });
345 }
346 r = rates;
347 c = cashflows;
348 periods = rate_length as u32;
349 } else if rate_length > 1 && cashflow_length == 2 {
350 r = rates;
351 periods = rate_length as u32;
352 for _i in 0..periods {
353 cashflow_vec.push(cashflows[1]);
354 }
355 c = &cashflow_vec;
356 } else if rate_length == 1 && cashflow_length > 2 {
357 c = cashflows;
358 periods = cashflow_length as u32 - 1;
359 for _i in 0..periods {
360 rate_vec.push(rates[0]);
361 }
362 r = &rate_vec;
363 } else {
364 return Err(crate::FinanceError::InvalidCashflow {
365 message: "rates or cashflows must provide a full series; only one may be a repeating shorthand",
366 });
367 }
368 Ok((periods, r.to_vec(), c.to_vec(), initial_investment))
369}
370
371/// Returns the net present value of a schedule of rates and cashflows (can be varying), subtracting the initial investment cost.
372/// Returns a custom solution struct with detailed information and additional functionality.
373///
374/// # Example
375/// ```
376/// let rates = vec![0.034, 0.034, 0.034];
377/// let cashflows = vec![-1000, 300, 400, 500];
378/// let npv = finance_solution::net_present_value_schedule_solution(&rates, &cashflows).unwrap();
379/// dbg!(npv.print_table());
380/// ```
381/// > outputs to terminal:
382/// ```text
383/// period rate present_value future_value investment_value
384/// ------ ------ ------------- ------------ ----------------
385/// 0 0.0000 -1_000.0000 -1_000.0000 -1_000.0000
386/// 1 0.0340 290.1354 300.0000 -709.8646
387/// 2 0.0340 374.1269 400.0000 -335.7377
388/// 3 0.0340 452.2810 500.0000 116.5433
389/// ```
390pub fn net_present_value_schedule_solution<C>(
391 rates: &[f64],
392 cashflows: &[C],
393) -> crate::FinanceResult<NpvSolution>
394where
395 C: Into<f64> + Copy,
396{
397 let (periods, rates, cashflows, initial_investment) = check_schedule(rates, cashflows)?;
398
399 let mut sum_accumulator = 0_f64;
400 let mut pv_accumulator = 0_f64;
401 for i in 0..periods {
402 let present_value = -present_value(
403 rates[i as usize],
404 (i + 1) as u32,
405 cashflows[i as usize + 1],
406 false,
407 )?;
408 pv_accumulator += present_value;
409 sum_accumulator += cashflows[i as usize + 1];
410 }
411 let sum_of_cashflows = sum_accumulator;
412 let sum_of_discounted_cashflows = pv_accumulator;
413 let net_present_value = initial_investment + pv_accumulator;
414
415 Ok(NpvSolution::new(
416 rates,
417 periods,
418 initial_investment,
419 cashflows,
420 sum_of_cashflows,
421 sum_of_discounted_cashflows,
422 net_present_value,
423 ))
424}
425
426/// The custom solution information of a NPV scenario.
427/// The struct values are immutable by the user of the library.
428#[derive(Debug)]
429pub struct NpvSolution {
430 rates: Vec<f64>,
431 periods: u32,
432 cashflows: Vec<f64>,
433 initial_investment: f64,
434 sum_of_cashflows: f64,
435 sum_of_discounted_cashflows: f64,
436 net_present_value: f64,
437}
438impl NpvSolution {
439 /// Create a new instance of the struct
440 pub fn new(
441 rates: Vec<f64>,
442 periods: u32,
443 initial_investment: f64,
444 cashflows: Vec<f64>,
445 sum_of_cashflows: f64,
446 sum_of_discounted_cashflows: f64,
447 net_present_value: f64,
448 ) -> Self {
449 Self {
450 rates,
451 periods,
452 initial_investment,
453 cashflows,
454 sum_of_cashflows,
455 sum_of_discounted_cashflows,
456 net_present_value,
457 }
458 }
459
460 pub fn series(&self) -> NpvSeries {
461 net_present_value_schedule_series(self)
462 }
463
464 /// Call `rate_avg` on a NpvSolution to get the simple average rate of a schedule;
465 pub fn rate_avg(&self) -> f64 {
466 let mut rate_accumulator = 0_f64;
467 for r in &self.rates {
468 rate_accumulator = rate_accumulator + r;
469 }
470 rate_accumulator / self.periods as f64
471 }
472
473 /// Returns the rate schedule
474 pub fn rates(&self) -> &[f64] {
475 &self.rates
476 }
477 /// Returns the number of periods as u32.
478 pub fn periods(&self) -> u32 {
479 self.periods
480 }
481 /// Returns the initial investment as f64.
482 pub fn initial_investment(&self) -> f64 {
483 self.initial_investment
484 }
485 /// Returns a Vec<f64> of the cashflows.
486 pub fn cashflows(&self) -> &[f64] {
487 &self.cashflows
488 }
489 /// Returns the sum of the cashflows at their future value.
490 pub fn sum_of_cashflows(&self) -> f64 {
491 self.sum_of_cashflows
492 }
493 /// Returns the sum of the cashflows at their present value.
494 pub fn sum_of_discounted_cashflows(&self) -> f64 {
495 self.sum_of_discounted_cashflows
496 }
497 /// Returns the net present value as f64.
498 pub fn net_present_value(&self) -> f64 {
499 self.net_present_value
500 }
501 /// Alias for net_present_value().unwrap()
502 pub fn npv(&self) -> f64 {
503 self.net_present_value
504 }
505
506 /// Pretty-print a table of the calculations at each period for visual analysis.
507 pub fn print_table(&self) {
508 self.series().print_table();
509 }
510
511 /// Pretty-print a table of the calculations at each period for visual analysis, and provide a Locale for monetary formatting and preferred decimal precision.
512 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
513 self.series().print_table_locale(locale, precision);
514 }
515
516 /// Max discounted cashflow among periods after the initial investment, if any.
517 pub fn max_discounted_cashflow(&self) -> Option<f64> {
518 self.series().max_discounted_cashflow()
519 }
520 /// Min discounted cashflow among periods after the initial investment, if any.
521 pub fn min_discounted_cashflow(&self) -> Option<f64> {
522 self.series().min_discounted_cashflow()
523 }
524}
525
526#[derive(Debug)]
527pub struct NpvSeries(Vec<NpvPeriod>);
528impl NpvSeries {
529 pub(crate) fn new(series: Vec<NpvPeriod>) -> Self {
530 Self { 0: series }
531 }
532 pub fn filter<P>(&self, predicate: P) -> Self
533 where
534 P: Fn(&&NpvPeriod) -> bool,
535 {
536 Self {
537 0: self
538 .iter()
539 .filter(|x| predicate(x))
540 .map(|x| x.clone())
541 .collect(),
542 }
543 }
544
545 pub fn print_table(&self) {
546 self.print_table_locale_opt(None, None);
547 }
548
549 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
550 self.print_table_locale_opt(Some(locale), Some(precision));
551 }
552
553 fn print_table_locale_opt(
554 &self,
555 locale: Option<&num_format::Locale>,
556 precision: Option<usize>,
557 ) {
558 let columns = columns_with_strings(&[
559 ("period", "i", true),
560 ("rate", "f", true),
561 ("present_value", "f", true),
562 ("future_value", "f", true),
563 ("investment_value", "f", true),
564 ]);
565 let data = self
566 .iter()
567 .map(|entry| {
568 vec![
569 entry.period.to_string(),
570 entry.rate.to_string(),
571 entry.present_value.to_string(),
572 entry.future_value.to_string(),
573 entry.investment_value.to_string(),
574 ]
575 })
576 .collect::<Vec<_>>();
577 print_table_locale_opt(&columns, data, locale, precision);
578 }
579
580 pub fn print_ab_comparison(&self, other: &NpvSeries) {
581 self.print_ab_comparison_locale_opt(other, None, None);
582 }
583
584 pub fn print_ab_comparison_locale(
585 &self,
586 other: &NpvSeries,
587 locale: &num_format::Locale,
588 precision: usize,
589 ) {
590 self.print_ab_comparison_locale_opt(other, Some(locale), Some(precision));
591 }
592
593 fn print_ab_comparison_locale_opt(
594 &self,
595 other: &NpvSeries,
596 locale: Option<&num_format::Locale>,
597 precision: Option<usize>,
598 ) {
599 let columns = columns_with_strings(&[
600 ("period", "i", true),
601 ("rate_a", "f", true),
602 ("rate_b", "f", true),
603 ("present_value_a", "f", true),
604 ("present_value_b", "f", true),
605 ("future_value_a", "f", true),
606 ("future_value_b", "f", true),
607 ("investment_value_a", "f", true),
608 ("investment_value_b", "f", true),
609 ]);
610 let mut data = vec![];
611 let rows = max(self.len(), other.len());
612 for row_index in 0..rows {
613 data.push(vec![
614 row_index.to_string(),
615 self.get(row_index)
616 .map_or("".to_string(), |x| x.rate.to_string()),
617 other
618 .get(row_index)
619 .map_or("".to_string(), |x| x.rate.to_string()),
620 self.get(row_index)
621 .map_or("".to_string(), |x| x.present_value.to_string()),
622 other
623 .get(row_index)
624 .map_or("".to_string(), |x| x.present_value.to_string()),
625 self.get(row_index)
626 .map_or("".to_string(), |x| x.future_value.to_string()),
627 other
628 .get(row_index)
629 .map_or("".to_string(), |x| x.future_value.to_string()),
630 self.get(row_index)
631 .map_or("".to_string(), |x| x.investment_value.to_string()),
632 other
633 .get(row_index)
634 .map_or("".to_string(), |x| x.investment_value.to_string()),
635 ]);
636 }
637 print_table_locale_opt(&columns, data, locale, precision);
638 }
639
640 /// Max discounted cashflow among periods after the initial investment, if any.
641 pub fn max_discounted_cashflow(&self) -> Option<f64> {
642 self.iter()
643 .skip(1)
644 .map(|x| x.present_value())
645 .reduce(f64::max)
646 }
647
648 /// Min discounted cashflow among periods after the initial investment, if any.
649 pub fn min_discounted_cashflow(&self) -> Option<f64> {
650 self.iter()
651 .skip(1)
652 .map(|x| x.present_value())
653 .reduce(f64::min)
654 }
655}
656impl Deref for NpvSeries {
657 type Target = Vec<NpvPeriod>;
658
659 fn deref(&self) -> &Self::Target {
660 &self.0
661 }
662}
663
664#[derive(Clone, Debug)]
665pub struct NpvPeriod {
666 period: u32,
667 rate: f64,
668 present_value: f64,
669 future_value: f64,
670 investment_value: f64,
671 formula: String,
672 formula_symbolic: String,
673}
674impl NpvPeriod {
675 pub fn new(
676 period: u32,
677 rate: f64,
678 present_value: f64,
679 future_value: f64,
680 investment_value: f64,
681 formula: String,
682 formula_symbolic: String,
683 ) -> Self {
684 Self {
685 period,
686 rate,
687 present_value,
688 future_value,
689 investment_value,
690 formula,
691 formula_symbolic,
692 }
693 }
694 /// Returns the period number. The first real period is 1 but there's also a period 0 which
695 /// which shows the starting conditions.
696 pub fn period(&self) -> u32 {
697 self.period
698 }
699
700 /// Returns the periodic rate for the current period. If the containing struct is a
701 /// [`TvmSolution`] every period will have the same rate. If it's a [`TvmSchedule`] each period
702 /// may have a different rate.
703 pub fn rate(&self) -> f64 {
704 self.rate
705 }
706
707 /// Returns the present value of the cashflow.
708 pub fn present_value(&self) -> f64 {
709 self.present_value
710 }
711
712 /// Returns the future value of the cashflow.
713 pub fn future_value(&self) -> f64 {
714 self.future_value
715 }
716
717 /// Returns the investment value of the Npv scenario at the time of the current period.
718 pub fn investment_value(&self) -> f64 {
719 self.investment_value
720 }
721
722 /// Returns a text version of the formula used to calculate the value for the current period.
723 /// The formula includes the actual values rather than variable names. For the formula with
724 /// variables such as pv for present value call `formula_symbolic`.
725 pub fn formula(&self) -> &str {
726 &self.formula
727 }
728
729 /// Returns a text version of the formula used to calculate the value for the current period.
730 /// The formula includes variables such as r for the rate. For the formula with actual values
731 /// rather than variables call `formula`.
732 pub fn formula_symbolic(&self) -> &str {
733 &self.formula_symbolic
734 }
735}
736
737pub(crate) fn net_present_value_schedule_series(schedule: &NpvSolution) -> NpvSeries {
738 let mut series = vec![];
739
740 let periods = schedule.periods();
741 let mut investment_value = 0_f64;
742
743 for period in 0..=periods {
744 let rate = if period == 0 {
745 0.0
746 } else {
747 schedule.rates()[(period - 1) as usize]
748 };
749 let future_value = schedule.cashflows[period as usize];
750 let present_value = schedule.cashflows[period as usize] / (1. + rate).powf(period as f64);
751 // Inputs already validated at schedule construction; result should be finite.
752 debug_assert!(present_value.is_finite());
753 investment_value += present_value;
754 let formula = format!(
755 "{:.4} = {:.4} / (1 + {:.6})^{}",
756 present_value, future_value, rate, period
757 );
758 let formula_symbolic = "present_value = fv / (1 + rate)^periods".to_string();
759 series.push(NpvPeriod::new(
760 period,
761 rate,
762 present_value,
763 future_value,
764 investment_value,
765 formula,
766 formula_symbolic,
767 ))
768 }
769 NpvSeries::new(series)
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775 //use crate::*;
776
777 #[test]
778 fn test_net_present_value_1() {
779 let rate = 0.034;
780 let periods = 10;
781 let ii = -1000;
782 let cf = 500;
783 let npv = net_present_value(rate, periods, ii, cf).unwrap();
784 assert_approx_equal!(3179.3410288, npv);
785 }
786
787 #[test]
788 fn test_net_present_value_2() {
789 let rate = 0.034;
790 let periods = 400;
791 let ii = -1000;
792 let cf = 500;
793 let npv = net_present_value(rate, periods, ii, cf).unwrap();
794 assert_eq!(13_705.85948, (100_000. * npv).round() / 100_000.);
795 }
796
797 #[test]
798 fn test_net_present_value_3() {
799 let rates = vec![0.034, 0.089, 0.055];
800 let cashflows = vec![-1000, 200, 300, 500];
801 let npv = net_present_value_schedule(&rates, &cashflows).unwrap();
802 assert_eq!(-127.80162, (100_000. * npv).round() / 100_000.);
803 }
804
805 #[test]
806 fn test_net_present_value_4() {
807 let rates = vec![0.034, 0.089, 0.055];
808 let cashflows = vec![-1000, 200, 300, 500];
809 let npv = net_present_value_schedule_solution(&rates, &cashflows).unwrap();
810 assert_eq!(-127.80162, (100_000. * npv.npv()).round() / 100_000.);
811 }
812
813 #[test]
814 fn test_net_present_value_5() {
815 // wildcard use case: positive and negatives
816 let rates = vec![0.034, -0.0989, 0.055, -0.02];
817 let cashflows = vec![-1000, 1000, 500, -250, -250];
818 let npv = net_present_value_schedule_solution(&rates, &cashflows).unwrap();
819 assert_eq!(
820 98.950922304,
821 (10_000_000_000. * npv.npv()).round() / 10_000_000_000.
822 );
823 }
824}