Micro Tool Yard logo
Tools

Blog

The Timesheet Bug Hiding in Every Overnight Shift

Subtracting a clock-out time from a clock-in time works fine until the shift crosses midnight — then the naive subtraction goes negative, and the fix reveals why 'hours worked' is really a clock-arithmetic problem in disguise.

"Clock in at 9:00, clock out at 5:00, how many hours?" is ordinary subtraction. "Clock in at 10:00 PM, clock out at 6:00 AM" is not — subtract the two and you get a negative number, even though the person clearly worked eight hours. The bug isn't in the arithmetic; it's in treating clock times as if they were points on an unbroken number line instead of positions on a loop that resets every 24 hours.

Why the naive version breaks

A clock time like "6:00 AM" doesn't carry a date with it — it's just a position within a repeating 24-hour cycle. Converting both times to minutes-since-midnight and subtracting (clock-out minus clock-in) works perfectly as long as clock-out falls later in the same cycle. The moment a shift spans midnight, clock-out's minutes-since-midnight value is smaller than clock-in's, and the subtraction silently produces a negative duration instead of erroring — which is worse than a crash, because a negative number is easy to miss in a timesheet total until payroll runs and someone's hours come out wrong.

The fix: detect the wraparound, then add a day

The standard fix is a single conditional: if the subtraction comes out negative, add 24 hours (1,440 minutes) before using the result. This works because a negative result is the unambiguous signal that the clock wrapped around once — a shift can't legitimately run longer than 24 hours in a single "time in / time out" entry, so one wraparound is the only case worth handling. It's the same category of fix as handling overflow in modular arithmetic generally: detect that you've looped past the boundary, then correct by exactly one full cycle.

Breaks introduce their own edge case

Once an unpaid break is subtracted from the shift length, a second failure mode appears: what if the break is longer than the shift itself, say from a typo (90 minutes entered instead of 30)? Subtracting it naively produces a negative "hours worked," which is nonsensical — nobody works negative hours. Clamping the result to zero rather than letting it go negative turns a silent wrong number into an obviously-wrong-looking zero, which is far more likely to get caught and corrected by whoever's reading the timesheet.

Why decimal hours matter downstream

Once the worked duration is correctly computed as a base-60 hours:minutes value, most payroll and invoicing math still needs it converted to decimal hours (7:30 becomes 7.5) before it can be multiplied by an hourly rate — multiplying a rate directly against "30" (from "7:30") rather than against 0.5 hours is a surprisingly common source of billing errors when this conversion is done by hand across several shifts. Automating both the midnight-wraparound handling and the decimal-hours conversion is exactly what anhours calculator exists to do, so neither edge case has to be re-derived by hand every payroll cycle.