rust_finprim/rate/pct_change.rs
1use rust_decimal::prelude::*;
2
3/// Percentage Change
4///
5/// The percentage change is a measure of the relative change in value between two points in time.
6///
7/// # Arguments
8/// * `beginning_value` - The initial value or starting point
9/// * `ending_value` - The final value or ending point
10///
11/// # Returns
12/// * The percentage change as an `Option` containing a `Decimal` or `None` if there is a division by zero.
13///
14/// # Formula
15/// $$\\% \Delta = \frac{\mathrm{Ending\ Value} - \mathrm{Beginning\ Value}}{|\mathrm{Beginning\ Value}|}$$
16///
17/// # Example
18/// * Beginning value of $1000, ending value of $1500
19///
20/// ```
21/// use rust_finprim::rate::pct_change;
22/// use rust_decimal_macros::*;
23///
24/// let beginning_value = dec!(1000);
25/// let ending_value = dec!(1500);
26///
27/// let result = pct_change(beginning_value, ending_value);
28/// ```
29pub fn pct_change(beginning_value: Decimal, ending_value: Decimal) -> Option<Decimal> {
30 if beginning_value.is_zero() {
31 // Avoid division by zero
32 return None;
33 }
34
35 // Calculate the percentage change
36 Some((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/// use rust_decimal_macros::*;
71///
72/// let value = dec!(1000);
73/// let pct_change = dec!(0.5); // 50%
74///
75/// let result = apply_pct_change(value, pct_change);
76/// ```
77pub fn apply_pct_change(value: Decimal, pct_change: Decimal) -> Decimal {
78 pct_change * value.abs() + value
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use rust_decimal_macros::dec;
85
86 struct TestCase {
87 beginning_value: Decimal,
88 ending_value: Decimal,
89 pct_change: Decimal,
90 }
91
92 impl TestCase {
93 fn new(beginning_value: Decimal, ending_value: Decimal, pct_change: Decimal) -> Self {
94 TestCase {
95 beginning_value,
96 ending_value,
97 pct_change,
98 }
99 }
100 }
101
102 #[test]
103 fn test_pct_change_and_apply_pct_change() {
104 let cases = [
105 TestCase::new(dec!(1000), dec!(1500), dec!(0.5)), // 50% change
106 TestCase::new(dec!(1000), dec!(500), dec!(-0.5)), // -50% change
107 TestCase::new(dec!(1000), dec!(1000), dec!(0)), // 0% change
108 TestCase::new(dec!(1000), dec!(-1500), dec!(-2.5)), // -250% change
109 TestCase::new(dec!(-1000), dec!(1500), dec!(2.5)), // 250% change
110 ];
111 for case in &cases {
112 let pct_change_result = pct_change(case.beginning_value, case.ending_value);
113 assert_eq!(pct_change_result, Some(case.pct_change));
114
115 let apply_pct_change_result = apply_pct_change(case.beginning_value, case.pct_change);
116 assert_eq!(apply_pct_change_result, case.ending_value);
117 }
118
119 // Test with zero beginning value
120 let result_zero = pct_change(dec!(0), dec!(1000)).unwrap_or(dec!(0));
121 assert_eq!(result_zero, dec!(0));
122 }
123}