Skip to main content

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}