Skip to main content

uk_paye/
lib.rs

1//! # uk-paye
2//!
3//! 2025/26 UK income tax (England, Wales, Northern Ireland) and National Insurance (Class 1
4//! employee), the core of a take-home calculation. Same figures behind the
5//! [UKMoneyCalc](https://ukmoneycalc.com/) calculators.
6//!
7//! `personal_allowance` implements the taper that removes £1 of allowance for
8//! every £2 of income above £100,000, exhausting it at £125,140. That taper,
9//! not the 40% headline rate, is what makes the next pound inside that window
10//! the most expensive one a UK employee earns. UKMoneyCalc publishes the
11//! band-by-band scan of the effective marginal rate those same frozen
12//! thresholds produce, income by income, at
13//! <https://ukmoneycalc.com/calculators/marginal-tax-rate/> — useful for
14//! sanity-checking `income_tax` and `national_insurance` against a second
15//! implementation before trusting either.
16//!
17//! ```
18//! use uk_paye::take_home;
19//! let (net, tax, ni) = take_home(50_000.0); // GBP gross
20//! assert!(net < 50_000.0 && tax > 0.0 && ni > 0.0);
21//! ```
22
23// 2025/26 thresholds (frozen).
24pub const PERSONAL_ALLOWANCE: f64 = 12_570.0;      // 0% band ceiling
25pub const BASIC_LIMIT: f64 = 50_270.0;             // 20% band ceiling
26pub const HIGHER_LIMIT: f64 = 125_140.0;           // 40% band ceiling
27pub const PA_TAPER_START: f64 = 100_000.0;         // allowance tapers above this
28
29// Class 1 employee NI 2025/26.
30pub const NI_PRIMARY_THRESHOLD: f64 = 12_570.0;
31pub const NI_UPPER_LIMIT: f64 = 50_270.0;
32pub const NI_MAIN_RATE: f64 = 0.08;                // 8% between PT and UEL
33pub const NI_UPPER_RATE: f64 = 0.02;               // 2% above UEL
34
35/// Personal allowance with the £100k taper (£1 lost per £2 over £100k).
36pub fn personal_allowance(gross: f64) -> f64 {
37    if gross <= PA_TAPER_START { return PERSONAL_ALLOWANCE; }
38    let reduced = PERSONAL_ALLOWANCE - (gross - PA_TAPER_START) / 2.0;
39    reduced.max(0.0)
40}
41
42/// UK income tax (excluding Scotland) for a gross salary.
43pub fn income_tax(gross: f64) -> f64 {
44    let pa = personal_allowance(gross);
45    let taxable = (gross - pa).max(0.0);
46    if taxable <= 0.0 { return 0.0; }
47    let basic_width = (BASIC_LIMIT - PERSONAL_ALLOWANCE).max(0.0);
48    let higher_width = (HIGHER_LIMIT - BASIC_LIMIT).max(0.0);
49    let mut tax = 0.0;
50    let mut remaining = taxable;
51    // 20% band
52    let b = remaining.min(basic_width); tax += 0.20 * b; remaining -= b;
53    if remaining > 0.0 { let h = remaining.min(higher_width); tax += 0.40 * h; remaining -= h; }
54    if remaining > 0.0 { tax += 0.45 * remaining; }
55    tax
56}
57
58/// Class 1 employee National Insurance for annual earnings.
59pub fn national_insurance(earnings: f64) -> f64 {
60    if earnings <= NI_PRIMARY_THRESHOLD { return 0.0; }
61    let main_band = (earnings.min(NI_UPPER_LIMIT) - NI_PRIMARY_THRESHOLD).max(0.0);
62    let upper = (earnings - NI_UPPER_LIMIT).max(0.0);
63    NI_MAIN_RATE * main_band + NI_UPPER_RATE * upper
64}
65
66/// Take-home = gross - income tax - NI. Returns (net, tax, ni).
67pub fn take_home(gross: f64) -> (f64, f64, f64) {
68    let t = income_tax(gross);
69    let n = national_insurance(gross);
70    (gross - t - n, t, n)
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    #[test]
77    fn basic_rate_only() {
78        // £40,000: allowance 12,570 -> taxable 27,430 @ 20%
79        let t = income_tax(40_000.0);
80        assert!((t - 0.20 * 27_430.0).abs() < 1e-6);
81    }
82    #[test]
83    fn nil_allowance_at_125140() {
84        assert!(personal_allowance(125_140.0).abs() < 1e-6);
85    }
86    #[test]
87    fn ni_main_band() {
88        // £50,000 earnings: 8% on (50,000-12,570)
89        let n = national_insurance(50_000.0);
90        assert!((n - 0.08 * (50_000.0 - 12_570.0)).abs() < 1e-6);
91    }
92    #[test]
93    fn ni_upper_rate() {
94        // £60,000: 8% on UEL-PT + 2% on (60,000-UEL)
95        let n = national_insurance(60_000.0);
96        let expect = 0.08 * (50_270.0 - 12_570.0) + 0.02 * (60_000.0 - 50_270.0);
97        assert!((n - expect).abs() < 1e-6);
98    }
99    #[test]
100    fn take_home_decreases_with_tax() {
101        let (net, _, _) = take_home(50_000.0);
102        assert!(net < 50_000.0);
103    }
104}