Javascript Zoned-Date library - fully DST support
Tags: javascript, timezone, internationalization, localization
Published: 2023-09-10 04:00:39

---
title: Javascript Zoned-Date library - fully DST support
published: true
description: This post introduces my newly publish Javascript zoned-date npm package 
tags: javascript, timezone, internationalization, localization
# cover_image: ![Timezone](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aco91gjfmr0jnmat0n3c.jpg)
# published_at: 2023-09-10 03:25 +0000
---

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](https://www.npmjs.com/package/zoned-date), aiming to fix all date-related issues in Javascript.

# Install

```bash
yarn add zoned-date
```

```javascript
import {ZonedDate, OffsetDate} from 'zoned-date'
// or
import ZonedDate from 'zoned-date/ZonedDate'
import OffsetDate from 'zoned-date/OffsetDate'
```

# 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](https://tc39.es/proposal-temporal/docs/ambiguity.html#resolving-time-ambiguity-in-temporal)

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

```javascript
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
```

Timezone conversion

```javascript
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)
```

DST support

```javascript
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')
}
```

# 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](https://github.com/tranvansang/zoned-date/blob/03b67c0783c1079498dc4a0d0186263f2b927e5e/ZonedDate.mjs#L9) options (`compatible`, `later`, `earlier`, `reject`) defined in [Temporal Proposal](https://tc39.es/proposal-temporal/docs/ambiguity.html#resolving-time-ambiguity-in-temporal).
Compared to existing library's solution such as `date-fns-tz` which provides a too simple implementation ([see implementation](https://github.com/marnusw/date-fns-tz/blob/a92e0ad017d101a0c50e39a63ef5d322b4d849f6/src/_lib/tzParseTimezone/index.js#L129)), and obviously this is not enough for DST.