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}