rust_finprim/amort_dep_tax/tax.rs
1use crate::FloatLike;
2
3/// Progressive Income Tax
4///
5/// # Arguments
6/// * `agi` - Adjusted Gross Income (AGI) for the tax year, your total income minus any above-the-line deductions
7/// * `deductions` - Any below-the-line deductions for the tax year (i.e. standard or itemized deductions)
8/// * `rate_table` - A slice of tuples representing the upper income of each bracket and its rate for the tax year `(bracket, rate)`,
9/// the last tuple should represent a number to infinity and the highest rate. In practice, the
10/// final bracket would the maximum number representable by the type (`FloatLike::MAX`).
11///
12/// # Returns
13/// * An option containing the total tax owed for the tax year based on the progressive rate table.
14/// If AGI is less than deductions, zero is returned (no tax owed).
15///
16/// If the rate table is not valid, i.e. the brackets are not sorted in ascending order or the last bracket
17/// is not set to infinity (FloatLike::MAX), None is returned. See `progressive_tax_unchecked` for an unchecked
18/// (unsafe) version of this function that skips the rate table validation.
19///
20/// # Examples
21/// ```
22/// use rust_finprim::amort_dep_tax::progressive_tax;
23///
24/// let rate_table = vec![
25/// (9_875.0, 0.10),
26/// (40_125.0, 0.12),
27/// (85_525.0, 0.22),
28/// (163_300.0, 0.24),
29/// (207_350.0, 0.32),
30/// (518_400.0, 0.35),
31/// (f64::MAX, 0.37)
32/// ];
33///
34/// let agi = 100_000.0;
35/// let deductions = 12_000.0;
36/// let tax = progressive_tax(agi, deductions, &rate_table);
37/// ```
38pub fn progressive_tax<T: FloatLike>(agi: T, deductions: T, rate_table: &[(T, T)]) -> Option<T> {
39 // Validate the rate table by checking that the brackets are sorted
40 // in ascending order. If not, None is returned.
41 if rate_table.windows(2).any(|w| w[0].0 > w[1].0) {
42 return None;
43 }
44
45 // Validate the last bracket is set to infinity (T::MAX)
46 if rate_table.last().unwrap().0 != T::MAX {
47 return None;
48 }
49
50 // The rate table has been validated
51 Some(progressive_tax_unchecked(agi, deductions, rate_table))
52}
53
54/// Progressive Income Tax - Unchecked Version
55///
56/// This is an unchecked version of the `progressive_tax` function that skips the rate table validation, may provide
57/// a performance boost in scenarios where the rate table is known to be valid.
58///
59/// # Arguments
60/// * `agi` - Adjusted Gross Income (AGI) for the tax year, your total income minus any above-the-line deductions
61/// * `deductions` - Any below-the-line deductions for the tax year (i.e. standard or itemized deductions)
62/// * `rate_table` - A slice of tuples representing the upper income of each bracket and its rate for the tax year `(bracket, rate)`,
63/// the last tuple should represent a number to infinity and the highest rate. In practice, the
64/// final bracket would the maximum number representable by the type (`FloatLike::MAX`).
65///
66/// # Returns
67/// * The total tax owed for the tax year based on the progressive rate table.
68/// If AGI is less than deductions, zero is returned (no tax owed).
69///
70/// # Examples
71/// ```
72/// use rust_finprim::amort_dep_tax::progressive_tax;
73///
74/// let rate_table = vec![
75/// (9_875.0, 0.10),
76/// (40_125.0, 0.12),
77/// (85_525.0, 0.22),
78/// (163_300.0, 0.24),
79/// (207_350.0, 0.32),
80/// (518_400.0, 0.35),
81/// (f64::MAX, 0.37)
82/// ];
83///
84/// let agi = 100_000.0;
85/// let deductions = 12_000.0;
86/// let tax = progressive_tax(agi, deductions, &rate_table);
87/// ```
88pub fn progressive_tax_unchecked<T: FloatLike>(agi: T, deductions: T, rate_table: &[(T, T)]) -> T {
89 // If AGI is less than deductions, return zero (no tax owed)
90 // This is a common scenario for students or individuals with low income
91 if agi <= deductions {
92 return T::zero();
93 }
94
95 // Taxable income is AGI minus deductions
96 let taxable_income = agi - deductions;
97
98 let mut prev_bracket = T::zero();
99 let mut total_tax = T::zero();
100 for &(bracket, rate) in rate_table.iter() {
101 // if the taxable income is less than or equal to the previous bracket,
102 // break out of the loop - we're done
103 if taxable_income <= prev_bracket {
104 break;
105 }
106
107 // Calculate the tax owed in the current bracket
108 let taxable_in_bracket = (taxable_income.min(bracket) - prev_bracket).max(T::zero());
109 total_tax += taxable_in_bracket * rate;
110 prev_bracket = bracket;
111 }
112
113 total_tax
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[cfg(not(feature = "std"))]
121 extern crate std;
122 #[cfg(not(feature = "std"))]
123 use std::{assert_eq, vec};
124
125 #[test]
126 fn test_progressive_tax() {
127 let agi = 60_489.25;
128 // Standard single filer deduction for 2024
129 let deductions = 14_600.0;
130 // 2024 Federal Income Tax Brackets
131 let rate_table = vec![
132 (11_600.0, 0.10),
133 (47_150.0, 0.12),
134 (100_525.0, 0.22),
135 (191_950.0, 0.24),
136 (243_725.0, 0.32),
137 (609_350.0, 0.35),
138 (f64::MAX, 0.37),
139 ];
140
141 let tax: f64 = progressive_tax(agi, deductions, &rate_table).unwrap();
142 assert_eq!(tax, 5_274.71);
143
144 // Failing rate table (out of order brackets)
145 let rate_table_bad = vec![
146 (47_150.0, 0.12),
147 (11_600.0, 0.10),
148 (100_525.0, 0.22),
149 (191_950.0, 0.24),
150 (243_725.0, 0.32),
151 (609_350.0, 0.35),
152 (f64::MAX, 0.37),
153 ];
154 assert_eq!(progressive_tax(agi, deductions, &rate_table_bad), None);
155
156 // AGI less than deductions
157 assert_eq!(progressive_tax(10_000.0, deductions, &rate_table), Some(0.0));
158 }
159}