<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lex</title>
    <description>The latest articles on DEV Community by Lex (@lexporter).</description>
    <link>https://dev.to/lexporter</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4034908%2Fc18db57f-18cb-4c15-b542-d5ac737812a5.jpeg</url>
      <title>DEV Community: Lex</title>
      <link>https://dev.to/lexporter</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lexporter"/>
    <language>en</language>
    <item>
      <title>The Salary Math Took an Hour. The Calculator Took a Month.</title>
      <dc:creator>Lex</dc:creator>
      <pubDate>Sat, 18 Jul 2026 08:36:50 +0000</pubDate>
      <link>https://dev.to/salaryhow/the-salary-math-took-an-hour-the-calculator-took-a-month-37nb</link>
      <guid>https://dev.to/salaryhow/the-salary-math-took-an-hour-the-calculator-took-a-month-37nb</guid>
      <description>&lt;p&gt;Multiply an hourly rate by hours per week, then by weeks per year. That's the core formula behind a salary calculator, and you can write it in one line. I recently built &lt;a href="https://salaryhow.com" rel="noopener noreferrer"&gt;SalaryHow&lt;/a&gt;, a browser-based calculator for comparing hourly and annual pay, and the arithmetic was genuinely the first hour of the project.&lt;/p&gt;

&lt;p&gt;Everything after that hour was about a harder question: what does it take for someone to &lt;em&gt;trust&lt;/em&gt; a number on a screen enough to use it in a real decision about a job offer?&lt;/p&gt;

&lt;p&gt;This post is about the engineering and UX decisions that question forced. None of them involve novel computer science. Most of them involve assumptions, precision, and the strange fragility of interfaces that recalculate while you type.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Formula Was the Easy Part
&lt;/h2&gt;

&lt;p&gt;Here's the thing nobody tells you before building a pay calculator: two correct implementations can disagree.&lt;/p&gt;

&lt;p&gt;Take $25 per hour. Is that $52,000 per year? Only if you assume 40 hours per week and 52 paid weeks. Assume 50 weeks (two unpaid weeks off) and it's $50,000. Assume 37.5 hours, common in plenty of workplaces, and the number moves again. Monthly pay is worse. Is a month annual divided by 12? Four weeks of pay? Something derived from 4.33 weeks? All three appear in the wild, and they produce visibly different numbers.&lt;/p&gt;

&lt;p&gt;None of these are bugs. They're assumptions, and every calculator embeds them whether the author noticed or not.&lt;/p&gt;

&lt;p&gt;So the first real engineering decision had nothing to do with code: pick defaults deliberately (40 hours, 52 weeks, 5 workdays, monthly = annual / 12), expose them as editable inputs, and state them near the results. A user who disagrees with an assumption can change it. A user who can't see the assumption just concludes your calculator is wrong, because their last paycheck says so.&lt;/p&gt;

&lt;p&gt;The after-tax estimate got the same treatment. It's a single user-adjustable estimated tax rate, labeled as an estimate, with no pretense of modeling actual brackets or withholding. Presenting a rough figure honestly beats presenting a precise-looking figure that's wrong for almost everyone. It's an estimate for comparison, not tax advice, and the interface says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Calculation Model, Not Five
&lt;/h2&gt;

&lt;p&gt;My first prototype had the conversion math living in three components. The hourly view computed annual its own way, the results table computed monthly its own way, and a summary sentence did its own multiplication inline.&lt;/p&gt;

&lt;p&gt;They disagreed within a day. A rounding difference here, a forgotten "weeks worked" input there, and the summary sentence contradicted the table above it. Nothing destroys trust in a calculator faster than the calculator disagreeing with itself on one screen.&lt;/p&gt;

&lt;p&gt;The fix was boring and absolute: one module owns the math. Every input, whichever direction the user is working in, gets normalized into an annual gross figure first. Every other number is derived from that single value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Everything flows through one normalized base.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;annualGross&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hourlyRate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;hoursPerWeek&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;weeksPerYear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;weekly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;annualGross&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;weeksPerYear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;daily&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;weekly&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;workdaysPerWeek&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;monthly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;annualGross&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;afterTax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;annualGross&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;estimatedTaxRate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Components render values. They never compute them. When someone works from the other direction and enters a salary, the same module derives the hourly rate from the same base, using the same assumptions.&lt;/p&gt;

&lt;p&gt;This sounds obvious written down. It's also the single decision that prevented the most bugs, because every future change (a new pay period, a tweak to how workdays affect daily pay) happens in exactly one place, and every displayed number stays consistent with every other one by construction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round Once, at the End
&lt;/h2&gt;

&lt;p&gt;Related lesson, learned the annoying way: intermediate rounding poisons everything downstream.&lt;/p&gt;

&lt;p&gt;Early on I rounded values as they were computed, because tidy numbers felt right. Then a user-visible problem appeared: monthly pay times 12 didn't equal the annual figure shown two rows up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// $47,000 / 12 = 3916.666...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;monthlyRounded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;3916.67&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;monthlyRounded&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 47,000.04&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four cents. Trivial mathematically, fatal for credibility, because the reader has no way to know the discrepancy is rounding and not a bug. If the calculator is four cents off on something they can verify, why believe it about anything they can't?&lt;/p&gt;

&lt;p&gt;The rule that fixed it: full floating-point precision internally, everywhere, always. Formatting happens exactly once, at the presentation layer, through one shared currency formatter (&lt;code&gt;Intl.NumberFormat&lt;/code&gt; handles this well). Internal values never round; displayed values always round the same way.&lt;/p&gt;

&lt;p&gt;I'd apply that rule to any tool that shows derived numbers, not just this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recalculating on Every Keystroke Is a UX Choice, Not a Free Win
&lt;/h2&gt;

&lt;p&gt;There's no submit button. Change any input and every result updates immediately. People clearly prefer this, and it's what makes comparing scenarios pleasant: nudge hours per week and watch the annual figure respond.&lt;/p&gt;

&lt;p&gt;It also creates a category of problems a submit button never has.&lt;/p&gt;

&lt;p&gt;While someone types "27.50", the input passes through "2", "27", "27.", and "27.5". A naive implementation recalculates on each intermediate state, so the results flash through nonsense before landing. Clear a field entirely and a careless implementation shows $0 everywhere or, worse, NaN, which reads as "this site is broken" to anyone who doesn't write JavaScript.&lt;/p&gt;

&lt;p&gt;The handling that ended up feeling right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parse leniently while typing. An empty or partial value keeps the last valid result on screen rather than zeroing everything.&lt;/li&gt;
&lt;li&gt;Treat a genuinely invalid value (negative rate, letters) as "hold the previous result and quietly mark the field," not as an error modal.&lt;/li&gt;
&lt;li&gt;Skip animations on the numbers. Early versions had a subtle count-up transition. It felt lively in a demo and exhausting in use, because real-time tools trigger it constantly. Numbers now just change.&lt;/li&gt;
&lt;li&gt;Keep layout rock-stable. Result rows have fixed positions and reserved widths, so a value growing from $9,000 to $190,000 doesn't reflow the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Immediate is good. Twitchy is not. The line between them is entirely in how you handle the in-between states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hiding the Advanced Options Without the Page Jumping
&lt;/h2&gt;

&lt;p&gt;Five inputs matter to almost everyone: the rate or salary, and hours per week. The rest (weeks worked, workdays, estimated tax rate) matter a lot to a minority and are noise to everyone else.&lt;/p&gt;

&lt;p&gt;So the &lt;a href="https://salaryhow.com/hourly-to-salary" rel="noopener noreferrer"&gt;hourly to salary calculator&lt;/a&gt; shows the essential fields by default and folds the rest into an expandable "adjust assumptions" section. First-time users see a form they can complete in five seconds. People with unusual schedules can open the panel and model 50 weeks or a four-day week.&lt;/p&gt;

&lt;p&gt;The mistake I made first: animating that panel open by pushing the results down. Expanding options shoved the numbers the user was looking at below the fold, which made the feature feel like a punishment for curiosity. The fix was reserving the layout so results stay anchored while options expand above or beside them, depending on viewport. Small thing. Completely changed whether anyone touched the advanced settings twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reversing the Calculation Isn't Swapping Two Fields
&lt;/h2&gt;

&lt;p&gt;I assumed the salary-to-hourly direction would be the hourly-to-salary form with inputs and outputs exchanged. Same math, after all.&lt;/p&gt;

&lt;p&gt;It isn't the same product. Someone converting a salary into an hourly figure is usually asking a different question: often "what is my time actually worth given the hours I really work," which makes the hours-per-week input the emotional center of the page instead of an afterthought. The &lt;a href="https://salaryhow.com/salary-to-hourly" rel="noopener noreferrer"&gt;salary to hourly calculator&lt;/a&gt; leads with different labels, emphasizes the effective hourly result over the pay-period table, and explains its assumptions in the opposite direction ("based on X hours per week" rather than "assuming 52 paid weeks").&lt;/p&gt;

&lt;p&gt;Defaults differ too. In the hourly direction, defaulting hours to 40 is a convenience. In the salary direction, 40 is an editorializing choice, because salaried people working 50 hours get a very different answer, and that difference is frequently the entire reason they came.&lt;/p&gt;

&lt;p&gt;Shared math module underneath, per the earlier lesson. Different interface on top. Direction changes what users mean, and the UI has to honor that even when the arithmetic doesn't care.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Number Without Context Is Half an Answer
&lt;/h2&gt;

&lt;p&gt;The results area went through more revisions than anything else, and the pattern of every revision was the same: add context, remove decoration.&lt;/p&gt;

&lt;p&gt;What survived:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A direct sentence above the table: "$25.00 per hour is about $52,000 per year, before taxes, at 40 hours per week." One line, answers the question, names the assumptions.&lt;/li&gt;
&lt;li&gt;A table of pay periods (daily, weekly, biweekly, monthly, annual) with unambiguous labels. "Biweekly" specifically says every two weeks, since a surprising number of people conflate it with twice a month.&lt;/li&gt;
&lt;li&gt;Gross and estimated after-tax shown as clearly separated figures, never mixed in one column.&lt;/li&gt;
&lt;li&gt;No cents on large annual figures. $52,000 reads as honest; $52,003.47 reads as false precision when the inputs are estimates anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The principle underneath all of it: the output should be understandable to someone who will never inspect the math, while remaining verifiable by someone who will.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Cases That Earn Trust
&lt;/h2&gt;

&lt;p&gt;Most users never enter zero weeks or a $4,000,000 salary. Testing those cases still matters, because the users who do poke at boundaries are exactly the skeptical ones deciding whether to trust you, and a NaN is disqualifying. Cases worth explicit tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;zero hours or zero weeks (results should degrade gracefully, not divide by zero)&lt;/li&gt;
&lt;li&gt;decimal rates like $27.53&lt;/li&gt;
&lt;li&gt;one workday per week&lt;/li&gt;
&lt;li&gt;50 or 51 weeks worked&lt;/li&gt;
&lt;li&gt;more than 40 hours per week&lt;/li&gt;
&lt;li&gt;0% tax and implausibly high tax rates&lt;/li&gt;
&lt;li&gt;clearing an input mid-typing&lt;/li&gt;
&lt;li&gt;switching calculation direction with values already entered (nothing should silently reset)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one is a five-minute test. Collectively they're the difference between a tool and a toy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Prioritize Again
&lt;/h2&gt;

&lt;p&gt;If I rebuilt this from scratch, the order of operations would change but four decisions would survive untouched.&lt;/p&gt;

&lt;p&gt;Write the assumptions down before writing any UI. Every ambiguity you resolve on paper (weeks per year, the meaning of "monthly") is a contradiction you never ship.&lt;/p&gt;

&lt;p&gt;Centralize the math ruthlessly. One module, one normalized base value, components that only render.&lt;/p&gt;

&lt;p&gt;Separate precision from presentation. Full precision inside, one formatter at the edge, rounding exactly once.&lt;/p&gt;

&lt;p&gt;Design for the in-between states first. Empty fields, half-typed decimals, and toggled options are where real-time tools live or die; the happy path was never the hard part.&lt;/p&gt;

&lt;p&gt;The broader lesson generalizes past salary math. Any interactive tool is really two products: the calculation, which takes an afternoon, and the trust, which takes the month. Users can't audit your code, so they audit what they can see: whether the numbers agree with each other, whether the assumptions are visible, whether the interface stays calm while they think. Build for that audit and the tool gets used. Skip it and correctness won't save you.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>news</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
