Skip to main content

dbe_cashflow/
lib.rs

1// src/econ/cashflow.rs
2#[inline]
3pub fn cal_pv(val: f64, rate: f64, step: f64) -> f64 {
4    assert!(rate > -1.0, "Discount rate must be greater than -100%");
5    assert!(step >= 0.0, "Step should generally be non-negative");
6
7    val / (1.0 + rate).powf(step)
8}
9
10pub fn cal_pv_from_cf(cf: &[f64], rate: f64) -> f64 {
11    if rate <= -1.0 {
12        return f64::NAN;
13    }
14
15    let discount_multiplier = 1.0 / (1.0 + rate);
16    let mut current_discount = 1.0;
17
18    let mut total_pv = 0.0;
19    for &val in cf {
20        total_pv += val * current_discount;
21        current_discount *= discount_multiplier;
22    }
23
24    total_pv
25}
26
27/// Spreading a target PV across selected future steps.
28///
29/// This function takes a target PV and returns the uniform nominal value that needs to
30/// be achieved at each timestep in order to aggregate to the target PV.
31///
32/// # Arguments
33/// * `t_pv` - The target present value to be achieved
34/// * `t_steps` - Number of steps (excluding step 0) into the future in which the target
35///   is expected to be achieved
36/// * `rate` - The discount rate against which the cashflow is evaluated at
37/// * `w_init` - Whether to account for an undiscounted year 0 value
38pub fn pv_unispread(t_pv: f64, t_steps: i32, rate: f64, w_init: bool) -> f64 {
39    assert!(t_steps >= 0, "Steps must be non-negative");
40    if t_steps < 0 || rate <= -1.0 {
41        return f64::NAN;
42    }
43
44    let start_idx = if w_init { 0 } else { 1 };
45
46    if start_idx > t_steps {
47        return f64::NAN;
48    }
49
50    let base = 1.0 + rate;
51    let mut current_factor = base.powi(start_idx);
52    let mut fact = 0.0;
53
54    for _ in start_idx..=t_steps {
55        fact += current_factor;
56        current_factor *= base;
57    }
58
59    t_pv / fact
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_cal_pv_with_std_inputs_returns_expected_val() {
68        let out = cal_pv(100.00, 0.12, 1.0);
69        assert!(
70            (out - 89.2857).abs() < 0.1,
71            "Result: {out}, Expected: 89.2857",
72        );
73    }
74
75    #[test]
76    fn test_cal_pv_from_cf_with_std_inputs_returns_expected_val() {
77        let _in: [f64; 2] = [100.00, 100.00];
78        let out = cal_pv_from_cf(&_in, 0.12);
79        assert!(
80            (out - 189.2857).abs() < 0.1,
81            "Result: {out}, Expected: 189.2857",
82        );
83    }
84
85    #[test]
86    fn test_pv_unispread_with_init_returns_expected_val() {
87        let out = pv_unispread(100.00, 2, 0.12, true);
88        assert!(
89            (out - 29.6348).abs() < 0.1,
90            "Result: {out}, Expected: 29.6348",
91        );
92    }
93
94    #[test]
95    fn test_pv_unispread_without_init_returns_expected_val() {
96        let out = pv_unispread(100.00, 2, 0.12, false);
97        assert!(
98            (out - 42.1159).abs() < 0.1,
99            "Result: {out}, Expected: 42.1159",
100        );
101    }
102
103    #[test]
104    #[should_panic(expected = "Discount rate must be greater than -100%")]
105    fn test_cal_pv_with_invalid_rate_panics() {
106        let _ = cal_pv(100.0, -1.5, 1.0);
107    }
108
109    #[test]
110    #[should_panic(expected = "Step should generally be non-negative")]
111    fn test_cal_pv_with_negative_step_panics() {
112        let _ = cal_pv(100.0, 0.12, -1.0);
113    }
114
115    #[test]
116    fn test_cal_pv_from_cf_with_invalid_rate_returns_nan() {
117        let _in: [f64; 2] = [100.00, 100.00];
118        let out = cal_pv_from_cf(&_in, -1.5);
119        assert!(out.is_nan());
120    }
121
122    #[test]
123    fn test_pv_unispread_with_invalid_rate_returns_nan() {
124        let out = pv_unispread(100.0, 2, -1.5, true);
125        assert!(out.is_nan());
126    }
127
128    #[test]
129    #[should_panic(expected = "Steps must be non-negative")]
130    fn test_pv_unispread_with_negative_steps_panics() {
131        let _ = pv_unispread(100.0, -2, 0.12, true);
132    }
133
134    #[test]
135    fn test_pv_unispread_with_start_idx_greater_than_t_steps_returns_nan() {
136        // start_idx is 1 (w_init is false), t_steps is 0. start_idx > t_steps.
137        let out = pv_unispread(100.0, 0, 0.12, false);
138        assert!(out.is_nan());
139    }
140}