finance_solution/cashflow/
nper.rs

1//! **Number of periods calculations for scenarios with payments.** Returns the number of periods required for a Present Value to achieve a Future Value with a given set 
2//! of annuities (payments) at a specified periodic rate. The only module still in beta.
3
4use log::{warn};
5use libm::log10;
6
7// To do before final: Excel says some calculations cannot be done, like pmt=-5 pv=1000 fv=2000  ...figure out how to replicate this logic 
8// should we separate the function into pv and fv? (i think so...)
9
10// To do in next release: add nper_due functions
11
12/// Returns the number of periods required for an annuity (payments) to reach a future value, at a specified periodic rate. Still in beta.
13///
14/// Related functions:
15/// * To calculate nper while retaining the input values use
16/// [`nper_solution`].
17/// 
18/// The NPER formula is:
19///
20/// Number of Periods = 
21/// LN( (payment - future_value * periodic_rate) / (payment + present_value * periodic_rate) ) 
22/// / LN(1 + periodic_rate)
23/// 
24/// ...where LN is the natural log (log10).
25/// 
26/// # Arguments
27/// * `periodic_rate` - The rate at which the investment grows or shrinks per period, expressed as a
28/// floating point number. For instance 0.05 would mean 5% growth. Often appears as `r` or `i` in
29/// formulas.
30/// * `payment` - The payment amount per period, also referred to as an annuity or cashflow. In this formula, it must be negative, and future value must be positive. Often payment appears as `pmt` or `C` (cashflow) in formulas.
31/// * `present_value` - The present value, or total value of all payments now. Often appears as `pv` in formulas.
32/// * `future_value` - The future value, a cash balance after the last payment is made and interest has accrued in each period. Often appears as `fv` in formulas.
33///
34/// # Panics
35/// The call will fail if `payment` is greater than or equal to 0, because the formula requires `payment` to be negative. 
36/// Additionally, `present_value` and `future_value` must be positive, or 0. However, both `present_value` and `future_value` cannot be 0, at least one value must be set.
37///
38
39/// Returns f64 for Number of Periods (NPER). 
40/// Receive the number of periods required for a present value to equal a future value based on a set of payments at an interest rate. 
41/// If there is no initial present value, use 0. 
42/// This is equivalent to the NPER function in Excel / Google Sheets.
43pub fn nper<C: Into<f64> + Copy, P: Into<f64> + Copy, F: Into<f64> + Copy>(periodic_rate: f64, payment: C, present_value: P, future_value: F) -> f64 {
44    nper_solution(periodic_rate, payment, present_value, future_value).periods
45}
46
47/// Returns f64 for Number of Periods (NPER). 
48/// Receive the number of periods required for a present value to equal a future value based on a set of payments (annuity) at an interest rate. 
49/// If there is no initial present value, use 0. 
50/// This is equivalent to the NPER function in Excel / Google Sheets.
51pub fn nper_solution<C: Into<f64> + Copy, P: Into<f64> + Copy, F: Into<f64> + Copy>(periodic_rate: f64, payment: C, present_value: P, future_value: F) -> NperSolution {
52    let pmt = payment.into();
53    let pv = present_value.into();
54    let fv = future_value.into();
55    assert!(pv >= 0.0);
56    assert!(fv >= 0.0);
57    assert!(pv + fv > 0.0, "Either present_value, and/or future_value, must be greater than 0.");
58    assert!(pmt < 0_f64, "The payment amount must be negative, same as Excel / Google Sheets."); // payment must be negative, same as Excel.
59    assert!(periodic_rate.is_finite(), "Rate must be finite.");
60    assert!(pmt.is_finite(), "Payment amount must be finite.");
61    assert!(pv.is_finite(), "Present Value amount must be finite.");
62    assert!(fv.is_finite(), "Future Value amount must be finite.");
63    
64    
65    
66    // LN((pmt - fv*r_)/(pmt + pv*r_))/LN(1 + r_)
67    let numerator = libm::log10( (pmt - fv * periodic_rate) / (pmt +  pv * periodic_rate) );
68    let num_periods = numerator / libm::log10(1. + periodic_rate); 
69    NperSolution::new(periodic_rate, num_periods, pmt, pv, fv)
70}
71
72#[derive(Debug)]
73pub struct NperSolution {
74    pub periodic_rate: f64,
75    pub periods: f64,
76    pub payment: f64,
77    pub present_value_total: f64,
78    pub future_value_total: f64,
79    pub formula: String,
80    
81}
82impl NperSolution {
83    pub fn new(periodic_rate: f64, periods: f64, payment: f64, present_value_total: f64, future_value_total: f64) -> Self {
84        let formula = format!("LOG10(({} - {}*{})/({} + {}*{})) / LOG10(1 + {})", payment, future_value_total, periodic_rate, payment, present_value_total, periodic_rate, periodic_rate);
85        Self {
86            periodic_rate,
87            periods,
88            payment,
89            present_value_total,
90            future_value_total,
91            formula,
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::*;
100
101    #[test]
102    fn test_nper_1() {
103        // normal cases
104        assert_eq!(round_6(27.7879559), round_6(nper(0.034, -500, 1000, 20_000)));
105        assert_eq!(round_6(59.76100743), round_6(nper(0.034, -50, 1000, 2_000)));
106        assert_eq!(round_6(25.68169193), round_6(nper(0.034, -50, 0, 2_000)));
107        assert_eq!(round_6(80.18661533), round_6(nper(0.034, -5, 0, 2_000)));
108        assert_eq!(round_6(106.3368288), round_6(nper(0.034, -200, 0, 200_000)));
109    }
110
111    #[should_panic]
112    #[test]
113    fn test_nper_2() {
114        // infinite cases
115        nper(1_f64/0_f64, -500, 1000, 20_000);
116        nper(0.034, -1_f64/0_f64, 1000, 20_000);
117        nper(0.034, -500, 1_f64/0_f64, 20_000);
118        nper(0.034, -500, 0, 1_f64/0_f64);
119    }
120
121    #[should_panic]
122    #[test]
123    fn test_nper_3() {
124        // positive pmt
125        nper(1_f64/0_f64, 500, 1000, 20_000);
126    }
127
128    #[should_panic]
129    #[test]
130    fn test_nper_4() {
131        // positive pmt
132        nper(1_f64/0_f64, 0, 1000, 20_000);
133    }
134
135    #[should_panic]
136    #[test]
137    fn test_nper_5() {
138        // negative 0
139        nper(1_f64/0_f64, -0, 1000, 20_000);
140    }
141
142}