DST transitions handled; gap or overlap slots are skipped or disambiguated
Why it matters
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.
Severity rationale
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.
Remediation
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.
Detection
-
ID:
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:
- Spring forward gap: 2:00 AM to 2:59 AM does not exist. Look for try/catch around timezone conversion, slot validation after generation, or IANA library usage that automatically skips invalid times.
- Fall back overlap: 1:00 AM to 1:59 AM occurs twice. Look for disambiguation logic (e.g., storing UTC offset explicitly, or flagging ambiguous times).
Examine files matching
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 }
External references
- cwe · CWE-682 — Incorrect Calculation — manual offset arithmetic produces invalid slot times during DST gaps
- iso-25010:2011 · functional-correctness — Functional Correctness — slot generation must not produce non-existent DST gap times
Taxons
History
- 2026-04-18·v1.0.0·Initial import from booking-calendar-availability·automated