Skip to main content

us_paycheck_tax/
lib.rs

1//! # us-paycheck-tax
2//!
3//! 2025 US federal income tax (progressive brackets + standard deduction) and FICA
4//! (Social Security + Medicare), the core of a paycheck/take-home calculation. Same
5//! figures behind the [StateWage](https://statewage.com/) paycheck calculator.
6//!
7//! ```
8//! use us_paycheck_tax::{take_home, FilingStatus};
9//! // $75,000 single: fed tax on (75000-15000) + FICA
10//! let (net, fed, fica) = take_home(75_000.0, FilingStatus::Single);
11//! assert!(net < 75_000.0 && fed > 0.0 && fica > 0.0);
12//! ```
13
14/// 2025 federal standard deductions.
15pub const STD_DEDUCTION_SINGLE: f64 = 15_000.0;
16pub const STD_DEDUCTION_MARRIED: f64 = 30_000.0;
17
18/// 2025 FICA.
19pub const SS_RATE: f64 = 0.062;
20pub const SS_WAGE_BASE: f64 = 176_100.0;
21pub const MEDICARE_RATE: f64 = 0.0145;
22pub const ADDL_MEDICARE_RATE: f64 = 0.009; // above threshold
23
24#[derive(Clone, Copy, PartialEq, Eq)]
25pub enum FilingStatus { Single, Married }
26
27/// Standard deduction for the filing status.
28pub fn standard_deduction(s: FilingStatus) -> f64 {
29    match s { FilingStatus::Single => STD_DEDUCTION_SINGLE, FilingStatus::Married => STD_DEDUCTION_MARRIED }
30}
31
32/// 2025 federal brackets as (ceiling, rate); ceiling = f64::INFINITY for the top bracket.
33pub fn brackets(s: FilingStatus) -> &'static [(f64, f64)] {
34    match s {
35        FilingStatus::Single => &[
36            (11_925.0, 0.10), (48_475.0, 0.12), (103_350.0, 0.22), (197_300.0, 0.24),
37            (250_525.0, 0.32), (626_350.0, 0.35), (f64::INFINITY, 0.37),
38        ],
39        FilingStatus::Married => &[
40            (23_850.0, 0.10), (96_950.0, 0.12), (206_700.0, 0.22), (394_600.0, 0.24),
41            (501_050.0, 0.32), (751_600.0, 0.35), (f64::INFINITY, 0.37),
42        ],
43    }
44}
45
46/// Progressive federal income tax on a *taxable* income (after deductions).
47pub fn federal_income_tax(taxable: f64, s: FilingStatus) -> f64 {
48    if taxable <= 0.0 { return 0.0; }
49    let mut tax = 0.0;
50    let mut prev = 0.0;
51    for &(ceil, rate) in brackets(s) {
52        if taxable <= prev { break; }
53        let top = taxable.min(ceil);
54        tax += (top - prev) * rate;
55        prev = ceil;
56        if ceil.is_infinite() { break; }
57    }
58    tax
59}
60
61/// FICA: Social Security (capped) + Medicare (+ additional Medicare above threshold).
62pub fn fica(gross: f64, s: FilingStatus) -> f64 {
63    let ss = SS_RATE * gross.min(SS_WAGE_BASE);
64    let medicare = MEDICARE_RATE * gross;
65    let threshold = match s { FilingStatus::Single => 200_000.0, FilingStatus::Married => 250_000.0 };
66    let addl = if gross > threshold { ADDL_MEDICARE_RATE * (gross - threshold) } else { 0.0 };
67    ss + medicare + addl
68}
69
70/// Take-home = gross - federal income tax (on taxable) - FICA. Returns (net, federal, fica).
71pub fn take_home(gross: f64, s: FilingStatus) -> (f64, f64, f64) {
72    let taxable = (gross - standard_deduction(s)).max(0.0);
73    let fed = federal_income_tax(taxable, s);
74    let f = fica(gross, s);
75    (gross - fed - f, fed, f)
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    #[test]
82    fn bracket_progression_single() {
83        // taxable 60,000 -> 10% on 11925 + 12% on (48475-11925) + 22% on (60000-48475)
84        let t = federal_income_tax(60_000.0, FilingStatus::Single);
85        let expect = 0.10*11_925.0 + 0.12*(48_475.0-11_925.0) + 0.22*(60_000.0-48_475.0);
86        assert!((t - expect).abs() < 1e-6);
87    }
88    #[test]
89    fn zero_taxable_zero_tax() {
90        assert_eq!(federal_income_tax(0.0, FilingStatus::Single), 0.0);
91    }
92    #[test]
93    fn fica_caps_ss() {
94        // gross far above wage base: SS only on 176,100
95        let f = fica(500_000.0, FilingStatus::Single);
96        let expect = 0.062*176_100.0 + 0.0145*500_000.0 + 0.009*(500_000.0-200_000.0);
97        assert!((f - expect).abs() < 1e-6);
98    }
99    #[test]
100    fn take_home_positive() {
101        let (net, fed, fica) = take_home(75_000.0, FilingStatus::Single);
102        assert!(net > 0.0 && fed > 0.0 && fica > 0.0);
103    }
104}