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}