Skip to main content

paycheck_utils/
lib.rs

1//! This library contains utility functions for calculating paycheck withholdings and net income given a hypothetical hourly wage and weekly working hours. The idea is pretty much like the "Sample Paycheck" tool found in the [Paycom](https://www.paycom.com/software/employee-self-service/) employee portal, but aimed at having a little more functionality and customization.
2//!
3//! The entire library was developed with the perspective of an hourly paid employee in mind, focusing on bi-weekly paychecks as the standard pay period to simulate how employees typically view and plan their income.
4//!
5//! The primary question this library aims to answer is: "Given an hourly wage and number of hours worked per week, what would my net paycheck be after taxes and deductions?"
6//!
7//! The secondary question this library aims to answer is: "Given a total monthly expenses amount and hourly wage, how many hours would I need to work to cover my expenses with "x" amount left over after taxes and deductions?" (this will be implemented in a future version).
8//!
9//! The library is structured into several modules:
10//! - `withholdings`: Contains functions to estimate federal tax withholdings, Social Security, and Medicare deductions.
11//! - `deductions`: Defines structures and functions for handling pre-tax and post-tax deductions.
12//! - `income`: Contains functions to calculate gross paycheck based on hourly wage and hours worked.
13//! - `expenses`: Defines structures and functions for managing monthly expenses.
14//! - `constants`: Contains tax and time related constants necessary for calculations.
15//! - `interaction`: Contains functions for interacting with the user to receive input for employment scenario.
16//! - `utils`: Contains utility functions for rounding and formatting output.
17//!
18//! A CLI will be implemented in a future version to allow a user to input their hourly wage, hours worked, filing status, deductions and expenses to see their estimated net paycheck and other relevant financial metrics.
19
20pub mod constants;
21pub mod deductions;
22pub mod expenses;
23pub mod income;
24pub mod interaction;
25pub mod utils;
26pub mod withholdings;
27
28pub use crate::constants::*;
29pub use crate::deductions::*;
30pub use crate::expenses::*;
31pub use crate::income::*;
32pub use crate::interaction::*;
33pub use crate::utils::*;
34pub use crate::withholdings::*;
35
36/// Represents an employment scenario with hourly rate, hours worked per week, filing status, and deductions.
37/// Possible deductions avaialable are defined in the `deductions` module.
38///
39/// # Example
40/// ```
41/// use paycheck_utils::*;
42///
43/// let new_job_scenario = EmploymentScenario::new(
44///     30.0, // hourly rate
45///     40.0, // hours per week
46///     FilingStatus::Single, // filing status
47///     PreTaxDeductions::new(vec![
48///         PreTaxDeduction::Medical(Some(150.0)),
49///         PreTaxDeduction::Dental(Some(50.0)),
50///         PreTaxDeduction::Vision(Some(15.0)),
51///         PreTaxDeduction::Traditional401K(Some(200.0)),
52///     ]), // pre-tax deductions
53///     PostTaxDeductions::new(vec![PostTaxDeduction::Roth401K(Some(100.0))]), // post-tax deductions
54///     Expenses::new(vec![]) // expenses
55/// );
56/// ```
57///
58#[derive(Default, Debug)]
59pub struct EmploymentScenario {
60    pub hourly_rate: f32,
61    pub hours_per_week: f32,
62    pub filing_status: FilingStatus,
63    pub pretax_deductions: PreTaxDeductions,
64    pub posttax_deductions: PostTaxDeductions,
65    pub expenses: Expenses,
66}
67
68impl EmploymentScenario {
69    pub fn new(
70        hourly_rate: f32,
71        hours_per_week: f32,
72        filing_status: FilingStatus,
73        pretax_deductions: PreTaxDeductions,
74        posttax_deductions: PostTaxDeductions,
75        expenses: Expenses,
76    ) -> Self {
77        EmploymentScenario {
78            hourly_rate,
79            hours_per_week,
80            filing_status,
81            pretax_deductions,
82            posttax_deductions,
83            expenses,
84        }
85    }
86
87    /// Calculates the net paycheck based on the employment scenario's parameters.
88    /// The calculations consider gross income, pre-tax deductions, federal tax withholdings, Social Security, Medicare, and post-tax deductions.
89    /// The IRS defined constants used to make calculations (such as tax rates, thresholds and standard deductions) are defined in the `constants` module.
90    /// This IRS method and flow for calculating withholdings is based on the 2026 federal tax year guidelines and can be summarized as follows:
91    ///    1. Calculate gross paycheck on hourly rate and hours worked.
92    ///    2. Subtract pre-tax deductions from gross paycheck to get adjusted gross paycheck.
93    ///    3. Calculate federal tax withholdings based on annualized adjusted gross paycheck and filing status.
94    ///    4. Calculate Social Security and Medicare withholdings based on adjusted gross paycheck.
95    ///    5. Subtract federal tax withholdings, Social Security, Medicare, and post-tax deductions from adjusted gross paycheck to get net paycheck.
96    ///
97    /// # Example
98    /// ```
99    /// use paycheck_utils::*;
100    ///
101    /// let pretax_deductions = PreTaxDeductions::new(vec![
102    ///     PreTaxDeduction::Medical(Some(100.0)),
103    ///     PreTaxDeduction::Dental(Some(50.0)),
104    ///     PreTaxDeduction::Vision(Some(25.0)),
105    ///     PreTaxDeduction::Traditional401K(Some(200.0)),
106    ///     PreTaxDeduction::HSA(Some(150.0)),
107    /// ]); // total = 525.0
108    /// let posttax_deductions = PostTaxDeductions::new(vec![
109    ///     PostTaxDeduction::Roth401K(Some(100.0)),
110    ///     PostTaxDeduction::VoluntaryLife(Some(30.0)),
111    /// ]); // total = 130.0
112    /// let scenario = EmploymentScenario::new(
113    ///     25.0, // hourly rate
114    ///     45.0, // hours per week (bi-weekly paycheck = 90 hours [10 hours overtime])
115    ///     FilingStatus::Single, // single filing status for standard deduction
116    ///     pretax_deductions, // total = 525.0
117    ///     posttax_deductions, // total = 130.0
118    ///     Expenses::new(vec![
119    ///         Expense::Housing(Some(2000.0)),
120    ///         Expense::Energy(Some(300.0)),
121    ///     ]), // total = 2300.0
122    /// );
123    /// let net_paycheck = scenario.calculate_net_paycheck();
124    /// assert_eq!(net_paycheck, 1440.33);
125    ///
126    /// // Explanation of calculation:
127    /// // 1. Gross Paycheck: (25.0 * 80) + (25.0 * 10 * 1.5) = 2000.0 + 375.0 = 2375.0
128    /// // 2. Adjusted Gross Paycheck: 2375.0 - 525.0 = 1850.0  (after pre-tax deductions)
129    /// // 3. Federal Withholding (annualized AGP = 1850.0 * 26 = 48100.0): Using 2026 tax brackets for Single filer:
130    /// //    - 10% on first 12,400 = 12,400 * 0.10 = 1,240.0
131    /// //    - 12% on amount over 12,400 up to 50,400 = (48,100.0 - 12,400.0) * 0.12 = 4,290.0
132    /// //    - Total annual federal tax = 1,240.0 + 4,290.0 = 5,530.0
133    /// //    - Bi-weekly federal withholding = 5,530.0 / 26 = 212.69
134    /// // 4. Social Security Withholding: 1850.0 * 0.062 = 114.70
135    /// // 5. Medicare Withholding: 1850.0 * 0.0145 = 26.83
136    /// // 6. Post-Tax Deductions: 100.0 + 30.0 = 130.0
137    /// // 7. Total Deductions: 212.69 + 114.70 + 26.83 + 130.0 = 484.22
138    /// // 8. Net Paycheck: 1850.0 - 212.69 - 114.70 - 26.83 - 130.0 = 1440.33
139    /// ```
140    /// # Returns
141    /// An `f32` representing the calculated net paycheck amount.
142    ///
143    /// # Panics
144    /// This function does not explicitly panic, but it assumes that the input values (hourly rate, hours worked, deductions) are valid and reasonable.
145    ///
146    /// # Errors
147    /// This function does not return errors, but invalid input values may lead to incorrect calculations.
148    ///
149    /// # Notes
150    /// The calculations are based on the 2026 federal tax year guidelines and may need to be updated for future tax years.
151    pub fn calculate_net_paycheck(&self) -> f32 {
152        let mut gross_paycheck = determine_gross_paycheck(self.hourly_rate, self.hours_per_week);
153        let total_pretax = self.pretax_deductions.total_pretax_deductions();
154        gross_paycheck -= total_pretax;
155        let federal_withholding =
156            estimate_paycheck_federal_withholdings(gross_paycheck, self.filing_status);
157        let social_security = estimate_social_security_withholding(gross_paycheck);
158        let medicare = estimate_medicare_withholding(gross_paycheck);
159        let total_posttax = self.posttax_deductions.total_posttax_deductions();
160
161        round_2_decimals(
162            gross_paycheck - federal_withholding - social_security - medicare - total_posttax,
163        )
164    }
165
166    /// Compares the total monthly expenses to the calculated monthly net income.
167    /// Returns a tuple containing the monthly net income, total monthly expenses, and the difference between the two.
168    /// # Example
169    /// ```
170    /// // This example uses the same data as the `calculate_net_paycheck` example to demonstrate the comparison.
171    ///
172    /// use paycheck_utils::*;
173    ///
174    /// let pretax_deductions = PreTaxDeductions::new(vec![
175    ///     PreTaxDeduction::Medical(Some(100.0)),
176    ///     PreTaxDeduction::Dental(Some(50.0)),
177    ///     PreTaxDeduction::Vision(Some(25.0)),
178    ///     PreTaxDeduction::Traditional401K(Some(200.0)),
179    ///     PreTaxDeduction::HSA(Some(150.0)),
180    /// ]); // total = 525.0
181    /// let posttax_deductions = PostTaxDeductions::new(vec![
182    ///     PostTaxDeduction::Roth401K(Some(100.0)),
183    ///     PostTaxDeduction::VoluntaryLife(Some(30.0)),
184    /// ]); // total = 130.0
185    /// let expenses = Expenses::new(vec![
186    ///     Expense::Housing(Some(1500.0)),
187    ///     Expense::Energy(Some(200.0)),
188    ///     Expense::Water(Some(50.0)),
189    ///     Expense::Groceries(Some(400.0)),
190    ///     Expense::Phone(Some(80.0)),
191    ///     Expense::Internet(Some(60.0)),
192    /// ]); // total = 2290.0
193    /// let scenario = EmploymentScenario::new(
194    ///     25.0, // hourly rate
195    ///     45.0, // hours per week
196    ///     FilingStatus::Single, // filing status
197    ///     pretax_deductions,
198    ///     posttax_deductions,
199    ///     expenses,
200    /// );
201    /// let (monthly_net_income, total_monthly_expenses, difference) = scenario.compare_monthly_expenses_to_monthly_income();
202    /// assert_eq!(monthly_net_income, 2880.66);
203    /// assert_eq!(total_monthly_expenses, 2290.0);
204    /// assert_eq!(difference, 590.66);
205    /// ```
206    /// # Returns
207    /// A tuple containing:
208    /// - `f32`: Monthly net income
209    /// - `f32`: Total monthly expenses
210    /// - `f32`: Difference between monthly net income and total monthly expenses
211    pub fn compare_monthly_expenses_to_monthly_income(&self) -> (f32, f32, f32) {
212        let monthly_net_income = self.calculate_net_paycheck() * 2.0;
213        let total_monthly_expenses = self.expenses.total_monthly_expenses();
214        (
215            round_2_decimals(monthly_net_income),
216            round_2_decimals(total_monthly_expenses),
217            round_2_decimals(monthly_net_income - total_monthly_expenses),
218        )
219    }
220}
221
222// UNIT TEST FOR LIBRARY
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    #[test]
228    fn test_calculate_net_paycheck() {
229        let pretax_deductions = PreTaxDeductions::new(vec![
230            PreTaxDeduction::Medical(Some(100.0)),
231            PreTaxDeduction::Dental(Some(50.0)),
232            PreTaxDeduction::Vision(Some(25.0)),
233            PreTaxDeduction::Traditional401K(Some(200.0)),
234            PreTaxDeduction::HSA(Some(150.0)),
235        ]);
236        let posttax_deductions = PostTaxDeductions::new(vec![
237            PostTaxDeduction::Roth401K(Some(100.0)),
238            PostTaxDeduction::VoluntaryLife(Some(30.0)),
239        ]);
240        let expenses = Expenses::new(vec![
241            Expense::Housing(Some(2000.0)),
242            Expense::Energy(Some(200.0)),
243            Expense::Water(Some(50.0)),
244            Expense::Internet(Some(60.0)),
245            Expense::Phone(Some(80.0)),
246            Expense::Vehicle(Some(300.0)),
247            Expense::VehicleInsurance(Some(150.0)),
248            Expense::VehicleGas(Some(100.0)),
249            Expense::Groceries(Some(400.0)),
250        ]);
251        let scenario = EmploymentScenario::new(
252            25.0,
253            45.0,
254            FilingStatus::Single,
255            pretax_deductions,
256            posttax_deductions,
257            expenses,
258        );
259        let net_paycheck = scenario.calculate_net_paycheck();
260        assert_eq!(net_paycheck, 1440.33);
261    }
262
263    #[test]
264    fn test_compare_monthly_expenses_to_monthly_income() {
265        let pretax_deductions = PreTaxDeductions::new(vec![
266            PreTaxDeduction::Medical(Some(100.0)),
267            PreTaxDeduction::Dental(Some(50.0)),
268            PreTaxDeduction::Vision(Some(25.0)),
269            PreTaxDeduction::Traditional401K(Some(200.0)),
270            PreTaxDeduction::HSA(Some(150.0)),
271        ]);
272        let posttax_deductions = PostTaxDeductions::new(vec![
273            PostTaxDeduction::Roth401K(Some(100.0)),
274            PostTaxDeduction::VoluntaryLife(Some(30.0)),
275        ]);
276        let expenses = Expenses::new(vec![
277            Expense::Housing(Some(1500.0)),
278            Expense::Energy(Some(200.0)),
279            Expense::Water(Some(50.0)),
280            Expense::Groceries(Some(400.0)),
281            Expense::Phone(Some(80.0)),
282            Expense::Internet(Some(60.0)),
283        ]);
284        let scenario = EmploymentScenario::new(
285            25.0,
286            45.0,
287            FilingStatus::Single,
288            pretax_deductions,
289            posttax_deductions,
290            expenses,
291        );
292        let (monthly_net_income, total_monthly_expenses, difference) =
293            scenario.compare_monthly_expenses_to_monthly_income();
294        assert_eq!(monthly_net_income, 2880.66);
295        assert_eq!(total_monthly_expenses, 2290.0);
296        assert_eq!(difference, 590.66);
297    }
298}