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