DST transitions create two temporal anomalies that break naive slot generators: a spring-forward gap (2:00–2:59 AM does not exist) and a fall-back overlap (1:00–1:59 AM occurs twice). CWE-682 (Incorrect Calculation) and ISO 25010 functional correctness both apply. A slot generator that iterates wall-clock minutes without DST awareness will produce bookable slots at 2:30 AM on the spring transition — a time that literally does not exist in that timezone. When a customer confirms such a booking, the confirmation timestamp resolves to an ambiguous or invalid time, causing silent downstream errors in reminders, calendar exports, and host notifications.
Info because DST failures affect only two days per year in affected timezones, but on those days they can produce invalid appointment times that propagate silently through reminders and exports.
Generate slots in UTC and convert to the display timezone using an IANA library rather than iterating local wall-clock minutes. IANA libraries skip spring-gap times automatically.
// src/lib/availability.ts
import { toZonedTime } from 'date-fns-tz'
export function generateSlots(date: Date, tz: string, stepMinutes = 30): Date[] {
const slots: Date[] = []
const startUtc = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
for (let i = 0; i < 24 * 60; i += stepMinutes) {
const utcSlot = new Date(startUtc.getTime() + i * 60_000)
try {
const zoned = toZonedTime(utcSlot, tz)
slots.push(zoned)
} catch {
// Non-existent DST gap time — skip
}
}
return slots
}
For fall-back overlaps, store the UTC value rather than the local wall-clock time so the two 1:30 AM slots are unambiguous in the database.
ID: booking-calendar-availability.timezone.dst-handling
Severity: info
What to look for: Search the codebase for DST-related handling. Count all slot generation functions that produce time slots for a given date range. For each, check if the function handles DST edge cases:
src/lib/*slot*, src/lib/*availability*, src/utils/*time*, src/utils/*date*.Pass criteria: At least 1 DST-aware mechanism must exist: IANA library usage (which handles gaps automatically), explicit try/catch around timezone conversion in slot generation, or slot validation that removes invalid times. The slot generation function must not produce bookable slots for non-existent DST gap times. Report even on pass: "DST handling mechanism: [IANA library/explicit check/try-catch]. Spring gap slots: [skipped/not handled]."
Fail criteria: Calendar allows bookings during the non-existent spring gap, or slot generation produces invalid times, or no DST awareness exists in the timezone conversion code.
Skip (N/A) when: Platform operates in a timezone with no DST (e.g., UTC, Asia/Kolkata), or platform does not support timezone conversion at all.
Detail on fail: Example: "On the spring DST transition (2:00 AM → 3:00 AM), the calendar allows booking a 2:30 AM slot. When the appointment is confirmed, the time is invalid in that timezone."
Cross-reference: Check iana-timezone-library — IANA libraries handle DST automatically; manual arithmetic does not.
Cross-reference: Check utc-storage — storing in UTC avoids DST ambiguity at the storage layer but display still needs DST awareness.
Cross-reference: Check customer-timezone-display — DST-aware display conversion is essential for timezones with DST.
Remediation: Use an IANA library to automatically skip invalid times:
import { toZonedTime, fromZonedTime } from 'date-fns-tz'
const tzName = 'America/New_York'
const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone
// Generate slots, skipping non-existent DST gap times
function generateSlotsWithDSTHandling(date, tzName) {
const slots = []
const startOfDay = new Date(date)
startOfDay.setHours(0, 0, 0, 0)
for (let i = 0; i < 24 * 60; i += 30) {
// 30-minute slots
const slotTime = new Date(startOfDay.getTime() + i * 60 * 1000)
// Try to convert to the target timezone
try {
const zonedTime = toZonedTime(slotTime, tzName)
// If conversion succeeds, the time is valid in this timezone
slots.push(zonedTime)
} catch {
// Time is invalid in this timezone (e.g., during DST gap), skip it
}
}
return slots
}