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
#!ek9 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: best practice, display, duration, local, locale, pattern, store, time, timezone, utc