Notes › Life Essays › Where the world clock offset comes from: one regex, and a hardcoded hour count that ignores daylight saving

Where the world clock offset comes from: one regex, and a hardcoded hour count that ignores daylight saving

The cross-continent meeting invite I sent at Sunday noon

2026-03-08T12:00:00.000Z is UTC Sunday noon. I sent Sydney, New York and London each a line saying "let us go live at three in the afternoon", checked it once, and then withdrew the invite: the same page was putting two numbers in front of me, one from a wall clock and one from an hour count hardcoded in a table.

1. The 13 cities are frozen into a constant

The city roster is a constant on lines 32-46 of the template, 13 entries, and only three fields are ever used: a name, an offset in hours, and a tz. The English name is not rendered at all. The tool list page copy reads "20+ major cities"; the measured array length is 13. Intl on this machine enumerates 418 time zones and the table covers 13 of them, so Taipei, Seoul, Toronto and Auckland are simply absent.

CityHardcoded offsetWhole-year mismatches in 2026
Beijing / Tokyo / Dubai / Moscow / Hawaii+8 / +9 / +4 / +3 / -100 samples
Sydney+108737 samples = 182.02 days
London / Paris / Berlin0 / +1 / +110080 each = 210 days
New York / Chicago / Denver / Los Angeles-5 / -6 / -7 / -811422 each = 237.958 days

Stepping through the year every 30 minutes gives 17473 sample points per city, 227149 across the 13 cities, and 84665 of them disagree, or 37.3%.

2. Who hands out the offset: one regex, and one hardcoded hour count

The label has two sources. The main path is on lines 92-112: it asks Intl for the shortOffset string and feeds that text to the regex on line 101 /GMT([+-])(\d{1,2})(?::(\d{2}))?/, and only a hit gets folded into minutes. A miss falls through to line 111 return fallbackOffset * 60, which is nothing but the hour count written in the table multiplied by 60.

The parse branch does get fractional offsets right: measured "GMT+5:30" yields 330 minutes, "GMT+8:45" yields 525 and "GMT-3:30" yields -210, and the label is printed as UTC+5:30. The fallback branch has a granularity of 60 minutes and can only ever emit whole hours; a bare "GMT" string and every abbreviation return null.

Swap the Intl option to short at the same instant and New York answers "EDT" while Los Angeles answers "PDT"; not one of the four American cities hits line 101, so the label drops back to the hardcoded value. That is the single door through which daylight saving time gets lost, and it is not the clock that is wrong, it is the label.

3. How many days each of the eight cities is off

Los Angeles 237.958 days New York 237.958 days Chicago 237.958 days Denver 237.958 days London 210 days Paris 210 days Berlin 210 days Sydney 182.02 days 17473 samples per city · 13 cities · 227149 in all 84665 mismatched = 37.3% · cities off 8/13

Fig. 1: Days in the year when the hardcoded offset and the real offset disagree

Three tiers: Sydney 182.02 days, London, Paris and Berlin 210 days apiece, and the four American cities 237.958 days apiece, that is 4368.5, 5040 or 5711 hours; the remaining five cities show 0 mismatches for the whole year. On the reference instant 1772971200000 the wall clock itself is fine, so this pile of days measures which number the label would fall back on.

4. The degraded path gets the wall clock wrong as well

The safety net on lines 115-129 never asks tzdb: line 116 getTimezoneOffset()*60000 first shifts the instant by the host offset, and only then do lines 121-125 read values back out of the shifted instant. Measured on the reference 1772971200000, five cities run an hour behind: Sydney 22:00:00 against a true 23:00:00, New York 07:00:00, and Chicago, Denver and Los Angeles the same way; the other eight cities sit at ±0h. Move the reference to 1784116800000, mid-July of the same year, and it is seven cities; move it to 1768478400000, in January, and only one city is left.

The second error is harder to see. Emulate a Los Angeles host (T=1772962200000) and the 13 cities split into two groups: seven are dragged an hour ahead, six display 0h, and Sydney lands exactly inside that 0h group, because "host plus an hour" and "hardcoded minus an hour" cancel each other out. It looks airtight while both halves are an hour wrong. When you are the one sending the invite, that kind of "it lines up" is the expensive one.

5. There is no "N hours apart" figure, you do the subtraction yourself

No two-city gap can be read off the page: <input, <select and <button match 0 places, and the word 相差 does not appear either. The only things you can subtract are the two labels, and the labels come from line 111 while the wall clock comes from lines 61-66, so on a switch day the two accounts each hand you a number. The pairs below are just two rows of that built-in roster, not a suggestion about which city is yours: Beijing to New York differs by 13h at 1772971200000 and by 12h 30 minutes later at 1772955000000; Beijing to London differs by 7h at 1792888200000 and by 8h at 1792891800000, so on the day the clock is set back the gap grows; Beijing to Sydney is -3h at 1775316600000 and -2h at 1775320200000.

The weekday is not shared either: at 1772947800000 Beijing is already Sunday 13:30 while Chicago, Denver, Los Angeles and Hawaii are still parked on Saturday. My sentence about "working in on Sunday" was a Saturday for half the recipients on that screen.

6. The badge only reads the hour

Line 152 t.hour >= 6 && t.hour < 18 decides whether a card shows the sun icon or the moon icon; the pair of thresholds on line 153 is 9 and 18, and they decide the green border plus the 工作时间 badge. Line 174 prints the weekday and neither test consults it. The reference day is a Sunday, yet 5 cards still wear the badge, with 8 sun icons and 5 moon icons at the same instant.

Daylight 6–18 Working hours 9–18 0 6 9 12 18 24 includes 09:00:00, excludes 18:00:00 · window spans 9 hours = 32400 s Neither test reads the weekday

Fig. 2: Two sets of hour thresholds drawn on one 24-hour axis

Boundary measurements: 1776257999000 false, 1776258000000 true, 1776290399000 true, 1776290400000 false.

7. It rewrites once a second and remembers nothing

There is exactly one timer, setInterval(render, 1000) on line 204, and line 185 rebuilds all 13 cards through a single innerHTML assignment, 8067 bytes each time, which by the code's own arithmetic comes to 86400 writes per day, roughly 664.7 MB per day. visibilitychange and clearInterval match 0 places, so a background tab keeps writing. The visitor time zone is matched by exact string equality, and only 13 of the 418 time zones are in the table: for Asia/Taipei and for Europe/Zurich the count of highlighted cards is 0, so a reader in Taipei or in Zurich never gets a highlighted home city at all.

Now I read the page as a lookup table: I trust only the wall-clock cell, and I copy a label into an email only after I have matched it against the hardcoded value on line 111. For any city missing from that roster I first have to find a separate tzdb reading outside the world clock tool; the millisecond-level checking follows the timestamp conversion post; and this trap of counting one thing two different ways is the same one I stepped in the every-28-days post.

Three questions people get backwards

Q: So is the page useless?

No, just read the time cell on the card: the wall clock runs through lines 61-66 and the label through line 111, and on a switch day those two split into two numbers.

Q: Does a different browser route around it?

No. Node and headless Chrome agree byte for byte on the same reference instant, and innerHTML is 8067 bytes in both.

Q: Which cities never get it wrong all year?

Beijing, Tokyo, Dubai, Moscow and Hawaii: those five have 0 mismatched samples. The other eight are wrong for at least 182.02 days and at most 237.958 days.

📎 Related articles

How many characters fit on a copybook page: the default A4 gives 20, not 100 ÷ 4 = 25 2026-10-27 A day on both the weekend list and the make-up list: which rule the workday tool obeys 2026-10-27 A Countdown Day Is 86400 Seconds, So DST Can Steal a Day 2026-10-27 One calculation prints two body fat percentages: 6.1 points apart by default, 0.7 per 1 cm of waist 2026-10-26 A photo rotated 90° lies flat again on another device: how the EXIF flag and a real rotation split the work 2026-10-22