rust_finprim/rate/
pct_change.rs

1use crate::FinPrimError;
2use crate::FloatLike;
3
4/// Percentage Change
5///
6/// The percentage change is a measure of the relative change in value between two points in time.
7///
8/// # Arguments
9/// * `beginning_value` - The initial value or starting point
10/// * `ending_value` - The final value or ending point
11///
12/// # Returns
13/// * The percentage change as a `Result` containing a `FloatLike` or `DivideByZero` error.
14///
15/// # Formula
16/// $$\\% \Delta = \frac{\mathrm{Ending\ Value} - \mathrm{Beginning\ Value}}{|\mathrm{Beginning\ Value}|}$$
17///
18/// # Example
19/// * Beginning value of $1000, ending value of $1500
20///
21/// ```
22/// use rust_finprim::rate::pct_change;
23///
24/// let beginning_value = 1000.0;
25/// let ending_value = 1500.0;
26///
27/// let result = pct_change(beginning_value, ending_value);
28/// ```
29pub fn pct_change<T: FloatLike>(beginning_value: T, ending_value: T) -> Result<T, FinPrimError<T>> {
30    if beginning_value.is_zero() {
31        // Avoid division by zero
32        return Err(FinPrimError::DivideByZero);
33    }
34
35    // Calculate the percentage change
36    Ok((ending_value - beginning_value) / beginning_value.abs()) // Use abs to ensure the division is correct for negative values
37}
38
39/// Apply Percentage Change
40///
41/// This function applies the percentage change to a given value and returns the new value.
42///
43/// # Arguments
44/// * `value` - The initial value or starting point
45/// * `pct_change` - The percentage change to apply
46///
47/// # Returns
48/// * The new value after applying the percentage change.
49///
50/// # Formula
51/// $$\mathrm{New\ Value} = |\mathrm{Value}| \times \\% \Delta + \mathrm{Value}$$
52///
53/// Fluctuations between pos and neg values are handled properly by using the absolute value as
54/// derived by the proper percentage change formula.
55///
56/// The more common formula for applying the percentage change is:
57///
58/// $$\mathrm{New\ Value} = \mathrm{Value} \times (1 + \\% \Delta)$$
59///
60/// However, this does not handle the cases where the value is negative and the percentage change is positive, its a
61/// simplification for when it can be assumed that the value is always positive.
62///
63/// For example, if EBITDA is -$1000 and EBITDA increased to -$500, the percentage change should/would be a pos. 50%
64/// but the latter formula would return -$1500 while the former would properly return -$500.
65///
66/// # Example
67/// * Value of $1000, percentage change of 50%
68/// ```
69/// use rust_finprim::rate::apply_pct_change;
70///
71/// let value = 1000.0;
72/// let pct_change = 0.5; // 50%
73///
74/// let result = apply_pct_change(value, pct_change);
75/// ```
76pub fn apply_pct_change<T: FloatLike>(value: T, pct_change: T) -> T {
77    pct_change * value.abs() + value
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    struct TestCase {
85        beginning_value: f64,
86        ending_value: f64,
87        pct_change: f64,
88    }
89
90    impl TestCase {
91        fn new(beginning_value: f64, ending_value: f64, pct_change: f64) -> Self {
92            TestCase {
93                beginning_value,
94                ending_value,
95                pct_change,
96            }
97        }
98    }
99
100    #[test]
101    fn test_pct_change_and_apply_pct_change() {
102        let cases = [
103            TestCase::new(1000.0, 1500.0, 0.5),   // 50% change
104            TestCase::new(1000.0, 500.0, -0.5),   // -50% change
105            TestCase::new(1000.0, 1000.0, 0.0),   // 0% change
106            TestCase::new(1000.0, -1500.0, -2.5), // -250% change
107            TestCase::new(-1000.0, 1500.0, 2.5),  // 250% change
108        ];
109        for case in &cases {
110            let pct_change_result = pct_change(case.beginning_value, case.ending_value);
111            assert_eq!(pct_change_result, Ok(case.pct_change));
112
113            let apply_pct_change_result = apply_pct_change(case.beginning_value, case.pct_change);
114            assert_eq!(apply_pct_change_result, case.ending_value);
115        }
116
117        // Test with zero beginning value
118        let result_zero = pct_change(0.0, 1000.0).unwrap_or(0.0);
119        assert_eq!(result_zero, 0.0);
120    }
121}