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}