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}