<?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: MAHER IKRAMAH</title>
    <description>The latest articles on DEV Community by MAHER IKRAMAH (@maher_ikramah_4a3da041672).</description>
    <link>https://dev.to/maher_ikramah_4a3da041672</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%2F4220613%2F31bcd72c-5fe8-47bc-a8c6-8f426fcbc497.png</url>
      <title>DEV Community: MAHER IKRAMAH</title>
      <link>https://dev.to/maher_ikramah_4a3da041672</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maher_ikramah_4a3da041672"/>
    <language>en</language>
    <item>
      <title>Building a Work-Hours Calculator? Time Arithmetic Is the Trap</title>
      <dc:creator>MAHER IKRAMAH</dc:creator>
      <pubDate>Sun, 01 Nov 2026 13:12:57 +0000</pubDate>
      <link>https://dev.to/maher_ikramah_4a3da041672/building-a-work-hours-calculator-time-arithmetic-is-the-trap-27g7</link>
      <guid>https://dev.to/maher_ikramah_4a3da041672/building-a-work-hours-calculator-time-arithmetic-is-the-trap-27g7</guid>
      <description>&lt;p&gt;If you have ever built a timesheet tool, a shift planner, or a payroll helper, you know the&lt;br&gt;
feature looks trivial until you write the first test. Start time minus end time, minus a&lt;br&gt;
break, times an hourly rate. What could go wrong? Plenty — and almost all of it comes from&lt;br&gt;
treating time like ordinary decimal numbers.&lt;/p&gt;

&lt;p&gt;This article walks through the four traps that bite every work-hours calculator, with the&lt;br&gt;
validation cases I use to check them. The examples are in JavaScript, but the lessons apply&lt;br&gt;
in any language.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Use minutes since midnight, not decimals
&lt;/h2&gt;

&lt;p&gt;The first mistake is storing times as decimals: 9:30 as &lt;code&gt;9.5&lt;/code&gt;, 5:45 PM as &lt;code&gt;17.75&lt;/code&gt;. It works&lt;br&gt;
until someone types 9:60, or until floating-point arithmetic turns 7.5 hours into&lt;br&gt;
7.4999999 and your pay figure comes out a cent short.&lt;/p&gt;

&lt;p&gt;Convert every time to minutes since midnight as soon as it enters your code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;09:00&lt;/code&gt; is &lt;code&gt;540&lt;/code&gt;, &lt;code&gt;17:00&lt;/code&gt; is &lt;code&gt;1020&lt;/code&gt;, and the shift length is &lt;code&gt;1020 - 540 = 480&lt;/code&gt;&lt;br&gt;
minutes. All of your arithmetic is integer arithmetic until the very last step, when you&lt;br&gt;
convert back to decimal hours for display (&lt;code&gt;480 / 60 = 8.00&lt;/code&gt;). Integers don't have&lt;br&gt;
rounding surprises; decimals do.&lt;/p&gt;

&lt;p&gt;The same rule applies to breaks. Store a break as minutes, not as &lt;code&gt;0.5&lt;/code&gt; hours. A 30-minute&lt;br&gt;
lunch is &lt;code&gt;30&lt;/code&gt;, and it stays &lt;code&gt;30&lt;/code&gt; through every calculation.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Subtract breaks before rounding, never after
&lt;/h2&gt;

&lt;p&gt;This is the bug that costs real money. Suppose a shift runs 9:00 AM to 5:00 PM with a&lt;br&gt;
30-minute break. Gross time is 8 hours; net time is 7.5 hours. At $20/hour, net pay is&lt;br&gt;
$150.00.&lt;/p&gt;

&lt;p&gt;Now suppose a developer rounds each piece first — shift length to the nearest hour, break&lt;br&gt;
to the nearest hour — and then subtracts. The 30-minute break rounds to 1 hour or 0 hours&lt;br&gt;
depending on the rule, and the result is off by half an hour. Multiply that by every shift&lt;br&gt;
in a pay period and you have a payroll correction waiting to happen.&lt;/p&gt;

&lt;p&gt;The rule: do all subtraction in minutes, and round only once, at the very end, for display.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;netMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;breakMinutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;toMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;toMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;breakMinutes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 9:00–17:00, 30-minute break&lt;/span&gt;
&lt;span class="nf"&gt;netMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;09:00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;17:00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 450 minutes = 7.50 hours&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Display rounding is a presentation choice. Calculation rounding is a correctness choice.&lt;br&gt;
Keep them separate, and keep the calculation in whole minutes until the final division.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Overnight shifts need modular arithmetic
&lt;/h2&gt;

&lt;p&gt;A shift from 10:00 PM to 6:00 AM breaks naive subtraction: &lt;code&gt;360 - 1320 = -960&lt;/code&gt; minutes.&lt;br&gt;
The fix is modular arithmetic — wrap the difference around a 24-hour day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shiftMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&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;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;toMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1440 = minutes in a day&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;shiftMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;22:00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;06:00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 480 minutes = 8.00 hours&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the edge case this creates: a start and end time that are equal now means a zero-length&lt;br&gt;
shift, not a 24-hour one. That is the safer default — a 24-hour shift should be entered&lt;br&gt;
explicitly, not inferred from identical times. And there is a second edge case worth&lt;br&gt;
deciding deliberately: which day does an overnight shift belong to? The day it starts, or&lt;br&gt;
the day it ends? Pick one, document it, and apply it consistently, because payroll&lt;br&gt;
periods and overtime thresholds depend on the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Validate with concrete cases, not vibes
&lt;/h2&gt;

&lt;p&gt;A calculator with no test cases is a guess. These are the cases I run before trusting any&lt;br&gt;
work-hours tool, and they cover every trap above:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;9:00 AM–5:00 PM, 30-minute break&lt;/td&gt;
&lt;td&gt;7.50 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9:00 AM–5:00 PM, 60-minute break&lt;/td&gt;
&lt;td&gt;7.00 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9:00 AM–5:00 PM, 30-minute break, $20/hour&lt;/td&gt;
&lt;td&gt;$150.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bi-weekly: 10 weekdays × 7.5 hours&lt;/td&gt;
&lt;td&gt;75.00 hours / $1,500 at $20/hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10:00 PM–6:00 AM overnight shift&lt;/td&gt;
&lt;td&gt;8.00 hours, not a negative number&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bi-weekly case matters because it catches a different class of bug: totals computed&lt;br&gt;
from rounded daily values instead of exact minutes. If each day is rounded before summing,&lt;br&gt;
ten days of 7.5 hours can drift. Sum the minutes first, convert once at the end, and 75.00&lt;br&gt;
comes out exactly.&lt;/p&gt;

&lt;p&gt;If you want a live example of this kind of tool to poke at, the free browser-based&lt;br&gt;
calculator at workhours-calculator.com runs the same arithmetic with no signup — useful as&lt;br&gt;
a reference for what the correct outputs look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Test the on-load state — it is a trust issue, not a cosmetic one
&lt;/h2&gt;

&lt;p&gt;Here is the trap most developers never think to test: what does the calculator show before&lt;br&gt;
the user touches anything?&lt;/p&gt;

&lt;p&gt;A calculator that loads with a headline result of 7.50 hours but a breakdown section full&lt;br&gt;
of zeroes has a bug, and every user who sees it will conclude the tool is broken — even if&lt;br&gt;
the math is perfect once they press "calculate." The same goes for the reverse: a&lt;br&gt;
breakdown that shows values while the headline shows nothing.&lt;/p&gt;

&lt;p&gt;The on-load state is a first impression, and for a tool that handles people's pay, first&lt;br&gt;
impressions are trust. Test it explicitly: load the page, change nothing, and check that&lt;br&gt;
every displayed number is consistent with every other one. Then change one input and check&lt;br&gt;
again. If your calculator computes on input change, the initial render must go through the&lt;br&gt;
exact same code path as a recalculation — not a separate, hand-written default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Convert times to minutes since midnight on input; do all math in integers.&lt;/li&gt;
&lt;li&gt;Subtract breaks in minutes, before any rounding; round once, for display only.&lt;/li&gt;
&lt;li&gt;Use modular arithmetic for overnight shifts, and decide which day they belong to.&lt;/li&gt;
&lt;li&gt;Validate against concrete cases: 7.50, 7.00, 75.00, and an overnight shift that is
not negative.&lt;/li&gt;
&lt;li&gt;Test the on-load state as rigorously as the calculations themselves.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Get those five right and your calculator will be boring in the best possible way: it will&lt;br&gt;
just give the right answer, every time.&lt;/p&gt;

&lt;p&gt;[Author bio — to be confirmed]&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
