Logo

dev-resources.site

for different kinds of informations.

Javascript Zoned-Date library - fully DST support

Published at
9/10/2023
Categories
javascript
timezone
internationalization
localization
Author
sang
Author
4 person written this
sang
open
Javascript Zoned-Date library - fully DST support

NPM package link: https://www.npmjs.com/package/zoned-date

Terminology

Regarding date time:

  • Wallclock: the values shown in your wall-clock, calendar, namely: year, month, date, day (weekday), hour, minute, second, millisecond, timezone offset.
  • Epoch: a point in the timeline stream, identified by the number of seconds from a specific time in history.

At the same moment, epoch is the same everywhere, but wallclock is different depending on the observation place. For example, at the moment, it is 1694393213485 milisec since the midnight at the beginning of January 1, 1970, UTC, this value is the same everywhere. But wallclock value at JST is 9:46AM, at UTC is 0:46AM.

Rationale

In Javascript, all wallclock methods returns different results based on the runtime's config. date.getHours() returns different results when running in client browser, in server, and in your local dev machine (for the date objects with same date.getTime() value).

Perfect fix for date-related problems

I recently publish zoned-date, aiming to fix all date-related issues in Javascript.

Install

yarn add zoned-date
Enter fullscreen mode Exit fullscreen mode
import {ZonedDate, OffsetDate} from 'zoned-date'
// or
import ZonedDate from 'zoned-date/ZonedDate'
import OffsetDate from 'zoned-date/OffsetDate'
Enter fullscreen mode Exit fullscreen mode

Usage

ZonedDate and OffsetDate implement all Date's methods with the additional of timezone support.

  • OffsetDate: when you know the offset of the timezone. This class is highly recommended. It is just math and the pure Date object, and always just works.
  • ZonedDate: you specify timezone by its name. The library uses Intl internally to derive the offset. Specially, ZonedDate explicitly support DST with the full support for Disambiguation option defined by Termporal proposal

Note: OffsetDate is sub-class of Date (new OffsetDate instanceof Date is true), while ZonedDate is not (new ZonedDate instanceof Date is false).

Sample usage

const date = new OffsetDate('2020-01-01T03:00:00.000Z', {offset: 9})
console.log(date.hours) // return hours at GMT+9: 12
date.hours = 10 // set hours at GMT+9
date.hours = h => h - 1 // decrease by 1
console.log(date.toISOString()) // 2020-01-01T00:00:00.000Z

date.withMonth(1).withYear(y => y + 1) // returns a new OffsetDate object
Enter fullscreen mode Exit fullscreen mode

Timezone conversion

const date = new ZonedDate('2021-09-04T05:19:52.001', {timezone: 'Asia/Tokyo'}) // GMT+9
console.log(date.hours === 5)

date.timezone = 'Asia/Bangkok' // GMT+7
console.log(date.hours === 5 - 9 + 7)

date.timezone = 'UTC'
console.log(date.hours === 5 - 9 + 24)

date.timezone = 'America/New_York' // GMT-4
console.log(date.hours === 5 - 9 + -4 + 24)
Enter fullscreen mode Exit fullscreen mode

DST support

for (const [timezone, wallclock, disambiguation, expected] of [
    // positive dst
    // forward, dst starts
    ['Australia/ACT', '2023-10-01T02:00:00.000', undefined, 11],
    ['Australia/ACT', '2023-10-01T02:30:00.000', undefined, 11],
    ['Australia/ACT', '2023-10-01T02:30:00.000', 'compatible', 11],
    ['Australia/ACT', '2023-10-01T02:30:00.000', 'earlier', 10],
    ['Australia/ACT', '2023-10-01T02:30:00.000', 'later', 11],
    // backward, dst ends
    ['Australia/ACT', '2023-04-02T02:00:00.000', undefined, 11],
    ['Australia/ACT', '2023-04-02T02:30:00.000', undefined, 11],
    ['Australia/ACT', '2023-04-02T02:30:00.000', 'compatible', 11],
    ['Australia/ACT', '2023-04-02T02:30:00.000', 'earlier', 11],
    ['Australia/ACT', '2023-04-02T02:30:00.000', 'later', 10],

    // negative dst
    // forward, dst starts
    ['America/Los_Angeles', '2023-03-12T02:00:00.000', undefined, -7],
    ['America/Los_Angeles', '2023-03-12T02:30:00.000', undefined, -7],
    ['America/Los_Angeles', '2023-03-12T02:30:00.000', 'compatible', -7],
    ['America/Los_Angeles', '2023-03-12T02:30:00.000', 'earlier', -8],
    ['America/Los_Angeles', '2023-03-12T02:30:00.000', 'later', -7],
    // backward, dst ends
    ['America/Los_Angeles', '2023-11-05T01:00:00.000', undefined, -7],
    ['America/Los_Angeles', '2023-11-05T01:30:00.000', undefined, -7],
    ['America/Los_Angeles', '2023-11-05T01:30:00.000', 'compatible', -7],
    ['America/Los_Angeles', '2023-11-05T01:30:00.000', 'earlier', -7],
    ['America/Los_Angeles', '2023-11-05T01:30:00.000', 'later', -8],
]) {
    const date = new ZonedDate(wallclock, {timezone, disambiguation})
    console.assert(date.offset === expected)
    console.log('ok')
}

for (const [timezone, wallclock, disambiguation, expected] of [
    ['Australia/ACT', '2023-10-01T02:30:00.000', 'reject'],
    ['Australia/ACT', '2023-04-02T02:30:00.000', 'reject'],
    ['America/Los_Angeles', '2023-03-12T02:00:00.000', 'reject'],
    ['America/Los_Angeles', '2023-11-05T01:00:00.000', 'reject'],
]) {
    const date = new ZonedDate(wallclock, {timezone, disambiguation})
    try {
      date.time
    } catch (e) {
      console.log('ok')
      continue
    }
    console.log('failed')
}
Enter fullscreen mode Exit fullscreen mode

Real-world use cases

We highly recommend using OffsetDate if you have a fixed timezone offset.

Suppose that you have a service in Japan (GMT+9), a dev team in India (GMT+5:30), the server in UTC (GMT), and some clients access the service from Los Angeles, California (which has DST).

When starting your client web app/or/(remote/local)server, you can do OffsetDate.defaultOffset = 9. After that, all calls to date.getFullYear, date.getMonth, etc. will return exactly the same values (which is the wallclock at GMT+9) in all runtime. You can confidently serialize the OffsetDate value (with date.toISOString(), or, date.getTime()), send/receive to/from client/(remote/local)server/database.

For example, if using the pure Date object, your client cannot specify Oct 01, 2023 02:30 AM to your service in Japan. because this wallclock does not exist in your client timezone.

OffsetDate internally overwrites all wallclock-related methods to shift the date to UTC timezone before the manipulation, so, any wallclock is supported.

Compare to existing libraries

Clean API interface (definitely)

OffsetDate is a sub-class of Date, you can pass OffsetDate instance to anything required Date. Additional properties are stored as private properties, they are not exposed without explicitly intention from lib author. Besides, we provide some convenient setter/getter and immutable edit, which are very straightforward, almost zero-brain muscle to memorize them.

Specifically, for example for fullYear, OffsetDate has:

  • const year = date.fullYear
  • date.fullYear = year
  • date.setFullYear(y => y + 1). We use Date's methods internally, so automatic shifting like date.setDate(-1) will work.
  • date.getFullYear()
  • date.withFullYear(2023). Immutable edit.

Operand for the assignment can be undefined (skip the assignment), number, (currentValue: number) => undefined (skip assignment), or, (currentValue: number) => number.

ZonedDate is not a sub-class of Date, but we implement all Date and OffsetDate's methods, so, if you pass ZonedDate to any places requiring Date instance, it mostly works.

Explicitly and sophisticated DST support for named Timezone.

We provide a sophisticated support for 3 DST disambiguation options (compatible, later, earlier, reject) defined in Temporal Proposal.
Compared to existing library's solution such as date-fns-tz which provides a too simple implementation (see implementation), and obviously this is not enough for DST.

timezone Article's
30 articles in total
Favicon
Converting date by user time zone in "NestJS", and entering and displaying date in "Angular"
Favicon
Timezone support in a full-stack application based on NestJS and Angular: working with REST and WebSockets
Favicon
Python: it is now() time to migrate from utcnow()
Favicon
geo2tz - 4 years later
Favicon
🌐 How to Change Time Zone in Google Chrome to Test Different Timezones
Favicon
¿Cómo trabajar correctamente con fechas?
Favicon
How to fix Dynamics AX 2012 R3 when the time is 1 hour ahead when not using daylight savings time
Favicon
Config Timezone Laravel in Your Project
Favicon
Javascript Zoned-Date library - fully DST support
Favicon
ChronoMate: Your Ultimate Time Zone Companion for Seamless Productivity and Scheduling Ease!
Favicon
supabase timezone + cron
Favicon
Command Prompt - Set Timezone
Favicon
Troubleshooting Timezone Issues in PostgreSQL with DBeaver
Favicon
Fixed: WHM (CPanel) + Laravel Timezone Issue
Favicon
Solve UK time changes (DST) with NodeJS and date-fns and Docker (epoch/unix format)
Favicon
Is there any reason to use ZoneId.of("UTC") instead of ZoneOffset.UTC?
Favicon
Debugging timezone issue in Java [Linux]
Favicon
Local time of employees in Notion
Favicon
Modificando a hora nas configurações do PHP8
Favicon
Time change in daylight saving
Favicon
Avoid this when using Date/Time functions in PHP
Favicon
Timezone for DateTime Field at Laravel Nova
Favicon
NTP (Network Time Protocol) Setup for Linux (Ubuntu) Server
Favicon
[Phoenix LiveView] formatting date/time with local time zone
Favicon
Shift the TZ for your K8s CronJobs
Favicon
Datetimes Are Hard: Part 2 - Writing and running code
Favicon
Where does GMT-0456 Timezone Come From?
Favicon
Dealing with Timezone in JavaScript
Favicon
Date and time gotchas
Favicon
How to avoid and debug most of timezone problems in production

Featured ones: