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}