How should I store dates in EK9 — UTC or local time?

← Date, Time, and Duration · Ref: Q547

Always store in UTC. Convert to local time only for display. This is the universal best practice and EK9 makes it natural.

WHY UTC FOR STORAGE

1. Unambiguous: UTC has no daylight saving transitions. 14:00 UTC is always 14:00 UTC.
2. Comparable: two UTC timestamps can be directly compared without timezone conversion.
3. Arithmetic: adding PT1H to a UTC time always moves forward exactly one hour. With local times, adding an hour across a DST boundary can give unexpected results.
4. Portable: UTC means the same thing everywhere. 'EST' could be US Eastern or Australian Eastern.

THE PATTERN

Store: always as UTC DateTime literals or UTC-constructed values.

  created <- 2024-06-15T14:00:00Z
  updated <- DateTime().now()

Display: convert to user's timezone with withSameInstant:

  userZone <- 'America/New_York'
  displayTime <- stored.withSameInstant(userZone)

Format: use Locale for regional formatting:

  enUS <- Locale('en_US')
  formatted <- enUS.longFormat(displayTime)

FULL PIPELINE

  stored <- 2024-06-15T14:00:00Z
  local <- stored.withSameInstant('Europe/Berlin')
  deDE <- Locale('de_DE')
  stdout.println(deDE.longFormat(local))

Store (UTC) to display (timezone) to format (locale) is the three-step pipeline.

See Q541 for timezone basics. See Q545 for timezone conversion. See Q546 for withSameInstant vs withZone. See Q549 for locale formatting. See Q553 for common timezone mistakes.

Example

defines module qa.store.utc.display.local

  defines program
    StoreUtcDisplayLocalDemo()
      stdout <- Stdout()

      // STORE: always in UTC
      orderCreated <- 2024-06-15T14:30:00Z
      orderShipped <- 2024-06-16T09:15:00Z
      stdout.println(`Stored (UTC): ${orderCreated}`)

      // COMPARE: UTC timestamps compare correctly
      require orderShipped > orderCreated
      elapsed <- orderShipped - orderCreated
      stdout.println(`Time to ship: ${elapsed}`)

      // DISPLAY: convert to user's timezone
      userZone <- "America/New_York"
      localCreated <- orderCreated.withSameInstant(userZone)
      localShipped <- orderShipped.withSameInstant(userZone)
      stdout.println(`User sees created: ${localCreated}`)
      stdout.println(`User sees shipped: ${localShipped}`)

      // FORMAT: locale-aware display
      enUS <- Locale("en_US")
      stdout.println(`Formatted: ${enUS.longFormat(localCreated)}`)

      // Full pipeline: store -> timezone -> locale
      deDE <- Locale("de_DE")
      berlinTime <- orderCreated.withSameInstant("Europe/Berlin")
      stdout.println(`German user sees: ${deDE.longFormat(berlinTime)}`)

      // Multiple users see different times for the same event
      tokyoTime <- orderCreated.withSameInstant("Asia/Tokyo")
      jaJP <- Locale("ja_JP")
      stdout.println(`Tokyo user sees: ${jaJP.longFormat(tokyoTime)}`)

      // All stored values stay UTC for correct comparison
      require localCreated == orderCreated
      require berlinTime == orderCreated
      require tokyoTime == orderCreated

Common mistakes

E50060 — DateTime has no toLocalTime() method. Use withSameInstant() to convert UTC timestamps to a local timezone for display. See ek9 -h E50060 for details.

Incorrect:

localCreated <- orderCreated.toLocalTime(userZone)

Correct:

localCreated <- orderCreated.withSameInstant(userZone)
Other ways to ask this
  • Should I store timestamps in UTC?
  • What is the best practice for storing dates?
  • How do I handle the store UTC display local pattern?

Coming from another language?

All languages: the 'store UTC, display local' pattern is universal best practice. Java: Instant for storage, ZonedDateTime for display. Python: datetime.utcnow() (deprecated) or datetime.now(UTC). JavaScript: Date internally stores UTC, toLocaleString() for display. Go: time.UTC for storage, In(loc) for display. EK9: DateTime literal with Z suffix for storage, withSameInstant() for display, Locale for formatting.

Keywords: timezone, duration, local, display, time, best practice, locale, store, pattern, utc