Dates & Time
Time zones: distinguish instants, dates and recurring schedules
Choose the right representation and test missing and repeated local times.
Decide what your value means
An event that already happened, a birthday and “every day at 09:00 in Madrid” are different data:
- Instant: store a timestamp with a documented unit or a text representation with an explicit offset. Render it in the required zone.
- Calendar date: keep the year, month and day when no time or zone belongs to the value. Inventing a midnight UTC instant can move the displayed date elsewhere.
- Local schedule: keep the local time, IANA zone and recurrence rule, plus a policy for missing/repeated times. A fixed UTC time alone does not preserve “09:00 Madrid” across offset changes.
The timestamp converter shows an instant in UTC and in the device’s zone. Its date input uses that device zone. The time-zone converter offers selected zones for comparison; use explicit UTC instants for the reproducible examples below. These tools are not a recurring-job scheduler.
Two instants around a spring transition
const format = new Intl.DateTimeFormat('en-GB', {
timeZone: 'Europe/Madrid', hour: '2-digit', minute: '2-digit',
hourCycle: 'h23', timeZoneName: 'longOffset'
});
console.log(format.format(new Date('2024-03-31T00:30:00Z')));
// 01:30 GMT+01:00
console.log(format.format(new Date('2024-03-31T01:30:00Z')));
// 03:30 GMT+02:00
Only one hour elapsed, while the displayed hour advanced by two. In that transition, the intervening local hour did not occur. A local 02:30 input needs a deliberate policy: reject it or resolve it under a documented rule. Not all regions change by one hour, and not all use daylight saving.
In the autumn example, 2024-10-27T00:30:00Z and 2024-10-27T01:30:00Z both display 02:30 in Madrid, with different offsets. A local time without offset cannot distinguish them.
Download and run the date checks with node date-checks.mjs. Assertions check hours and offsets through formatToParts, avoiding dependence on punctuation in the formatted text.
A zone name is not an offset
Europe/Madrid names a rule set, while +02:00 is one offset. For historical and future scheduling, use the zone and the applicable time-zone data. Abbreviations such as CST can refer to different regions. Keep runtime time-zone data updated; a zone name is not a promise that future political rules will remain unchanged.
Database choices depend on meaning
In PostgreSQL, timestamp with time zone (timestamptz) represents an instant and renders it using the session zone. It does not preserve the original IANA zone name. Store that separately when the application needs it. timestamp without time zone is useful for an intentionally local date/time; it does not silently acquire a zone just because a client expects one. A date column may be the right choice for a date-only value.
Do not transfer one database’s type advice blindly to another: ranges and conversion rules differ. Test the actual driver, column type and serialization path.
A compact test checklist
Check an ordinary date, a missing local time, a repeated local time, and a date near UTC midnight. Run in UTC and a non-UTC device zone. For a repeated schedule, test the next local occurrence, not simply the previous instant plus 24 hours. Document whether a timezone-less API value is a date, a local time or an error.
Sources: MDN Intl.DateTimeFormat, MDN Date, PostgreSQL date/time types, IANA Time Zone Database.