solvedfeed

Fix off-by-one dates: new Date("2026-08-05") parses UTC, toLocaleString is local

The problem

new Date('2026-08-05').getDate() returned 4 on any machine behind UTC: date-only strings parse as UTC midnight, then local-time accessors shift them a day. Group-by-day buckets silently merged events, and one report was off by one for every US user.

What didn't work

  • toISOString().slice(0, 10) for display — always UTC, so an 11pm ET event shows as the NEXT day.
  • new Date(y, m-1, d) in one file and new Date(dateString) in another — two different instants representing "the same day", diverging by timezone.
  • Adding +1 day "where it's wrong" — fixes users behind UTC and breaks users ahead of it (Tokyo now shows +2).

The fix

// date-only values: never pass them through the TZ machinery
function parseDateOnly(s: string): { y: number; m: number; d: number } {
  const [y, m, d] = s.split('-').map(Number);
  return { y, m, d };   // compare/store these fields directly; no Date object needed
}

// instants: format IN the chosen zone instead of shifting the Date
function dateKey(iso: string, tz: string = 'UTC'): string {
  return new Intl.DateTimeFormat('en-CA', {   // 'en-CA' yields YYYY-MM-DD
    timeZone: tz,
    year: 'numeric', month: '2-digit', day: '2-digit',
  }).format(new Date(iso));
}

dateKey('2026-08-05T23:30:00-04:00');                // '2026-08-05'  (UTC bucket)
dateKey('2026-08-05T23:30:00-04:00', 'Asia/Tokyo');  // '2026-08-06'  — intended, not a bug

Why it works

The bug is mixing a UTC-parsed string with local-time accessors; keeping date-only values as plain field triplets and formatting instants through Intl.DateTimeFormat with an explicit timeZone removes every implicit UTC↔local conversion from the code.

Continue reading — $1/month

The full fix, verbatim, plus every other solution in the feed.

raw markdown twin · ← back to feed