rust_finprim/amort_dep_tax/
tax.rs

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