The Hidden Complexity of Date Handling in Modern Apps

A practical guide to handling dates, times, and timezones in modern applications. Stop the datetime headaches.

Everyone thinks they know how to handle dates in their applications. Until they need to deal with timezones, DST changes, or calendar rules. Here's what you need to know.

The Common Pitfalls

1. Naive DateTime Storage

// ❌ BAD: Storing dates without context
interface Meeting {
  id: string;
  title: string;
  startTime: Date;  // Which timezone?
  endTime: Date;    // DST problems waiting to happen
}

// ❌ BAD: Timestamp assumptions
const meeting = {
  startTime: new Date('2024-03-10T02:30:00'),  // DST change in US
  endTime: new Date('2024-03-10T03:30:00')
};

2. Timezone Confusion

// ❌ BAD: Implicit timezone conversion
function scheduleInterview(dateStr: string) {
  const interviewTime = new Date(dateStr);
  // Is this UTC? Local? User's timezone?
  return db.interviews.create({
    scheduledFor: interviewTime
  });
}

// ❌ BAD: Mixing timezone handling
const local = new Date();
const utc = new Date(Date.UTC(
  local.getFullYear(),
  local.getMonth(),
  local.getDate()
));  // Still problematic

Better Approaches

1. Explicit Timezone Handling

import { DateTime } from 'luxon';

interface Meeting {
  id: string;
  title: string;
  startTime: string;        // ISO string
  timeZone: string;         // e.g., 'America/New_York'
  durationMinutes: number;  // Store duration, not end time
}

class MeetingService {
  schedule(meeting: CreateMeetingDTO): Meeting {
    const start = DateTime.fromISO(meeting.startTime, {
      zone: meeting.timeZone
    });

    // Validate business hours in correct timezone
    if (!this.isBusinessHour(start)) {
      throw new Error('Outside business hours');
    }

    return {
      ...meeting,
      startTime: start.toUTC().toISO(),
      endTime: start
        .plus({ minutes: meeting.durationMinutes })
        .toUTC()
        .toISO()
    };
  }

  private isBusinessHour(time: DateTime): boolean {
    const hour = time.hour;
    return hour >= 9 && hour < 17;
  }
}

2. Smart Display Components

// React component handling timezone display
interface TimeDisplayProps {
  isoString: string;
  userTimeZone: string;
  format?: string;
}

function TimeDisplay({ 
  isoString, 
  userTimeZone, 
  format = 'ff' 
}: TimeDisplayProps) {
  const time = DateTime.fromISO(isoString).setZone(userTimeZone);
  
  return (
    <time dateTime={isoString}>
      {time.toFormat(format)}
      <span className="text-gray-500 text-sm">
        ({time.zoneName})
      </span>
    </time>
  );
}

// Usage
<TimeDisplay 
  isoString="2024-03-15T14:30:00Z"
  userTimeZone="America/New_York"
  format="fff"
/>

3. Recurring Events

class RecurringMeeting {
  constructor(
    private readonly rule: string,  // RRULE format
    private readonly baseTime: DateTime,
    private readonly timeZone: string
  ) {}

  getNextOccurrences(count: number): DateTime[] {
    const rrule = rrulestr(this.rule, {
      dtstart: this.baseTime.toJSDate()
    });

    return rrule.all((date, i) => i < count)
      .map(date => 
        DateTime.fromJSDate(date)
          .setZone(this.timeZone)
      );
  }

  isValidOccurrence(date: DateTime): boolean {
    // Check if date matches rule
    const rrule = rrulestr(this.rule, {
      dtstart: this.baseTime.toJSDate()
    });

    return rrule.between(
      date.minus({ minutes: 1 }).toJSDate(),
      date.plus({ minutes: 1 }).toJSDate()
    ).length > 0;
  }
}

Best Practices

1. Storage

interface DateTimeEntity {
  timestamp: string;    // ISO 8601 in UTC
  timezone: string;     // IANA timezone
  originalTime: string; // Local time as entered by user
}

class DateTimeStorage {
  store(input: DateTimeInput): DateTimeEntity {
    const dt = DateTime.fromISO(input.time, {
      zone: input.timezone
    });

    return {
      timestamp: dt.toUTC().toISO(),
      timezone: input.timezone,
      originalTime: input.time
    };
  }
}

2. API Responses

interface TimeResponse {
  iso: string;      // For machine consumption
  timezone: string; // User's timezone
  display: {
    local: string;  // Formatted in user's timezone
    original: string; // As entered
    utc: string;    // UTC time
  };
}

class TimeResponseBuilder {
  build(entity: DateTimeEntity, userTz: string): TimeResponse {
    const time = DateTime.fromISO(entity.timestamp)
      .setZone(userTz);

    return {
      iso: entity.timestamp,
      timezone: userTz,
      display: {
        local: time.toFormat('ff'),
        original: entity.originalTime,
        utc: time.toUTC().toFormat('ff')
      }
    };
  }
}

Key Takeaways

  1. Always Store:
    • UTC timestamps
    • Original timezone
    • Original local time
  2. Never Assume:
    • User's timezone
    • DST rules
    • Calendar rules
  3. Use Good Tools:
    • Luxon/date-fns-tz for modern apps
    • Moment-timezone for legacy
    • IANA timezone database

Remember: Date handling is complex because time itself is complex. Don't reinvent the wheel - use proven libraries and patterns.