Best practices when working with universal time

It is a normal situation that the server, client, and database have different timezones.
In this blog, I will introduce my coding practice to handle multiply timezones in software development.
Check the timezone setting of the current machine
Method 1: use the date command and check the timezone part (JST or +07) of the output:
On Linux (archlinux):
# date
Tue Aug 9 01:01:42 AM JST 2022
On macOS:
# date
Mon Aug 8 23:01:42 +07 2022
Method 2: use command ls -al /etc/localtime
On Linux (Ubuntu):
# ls -al /etc/localtime
lrwxrwxrwx 1 root root 30 Oct 31 06:01 /etc/localtime -> /usr/share/zoneinfo/Asia/Tokyo
On Linux (Archlinux)
# ls -al /etc/localtime
lrwxrwxrwx 1 root root 32 Jun 4 10:16 /etc/localtime -> ../usr/share/zoneinfo/Asia/Tokyo
On macOS:
# ls -al /etc/localtime
lrwxr-xr-x 1 root wheel 42 Aug 3 16:12 /etc/localtime -> /var/db/timezone/zoneinfo/Asia/Ho_Chi_Minh
To list all timezone names
On Linux (Archlinux)
# ls -al /usr/share/zoneinfo
total 452
drwxr-xr-x 20 root root 4096 May 25 15:44 .
drwxr-xr-x 275 root root 12288 Jul 31 20:59 ..
drwxr-xr-x 2 root root 4096 May 25 15:44 Africa
drwxr-xr-x 6 root root 4096 May 25 15:44 America
drwxr-xr-x 2 root root 4096 May 25 15:44 Antarctica
drwxr-xr-x 2 root root 4096 May 25 15:44 Arctic
drwxr-xr-x 2 root root 4096 May 25 15:44 Asia
drwxr-xr-x 2 root root 4096 May 25 15:44 Atlantic
drwxr-xr-x 2 root root 4096 May 25 15:44 Australia
drwxr-xr-x 2 root root 4096 May 25 15:44 Brazil
drwxr-xr-x 2 root root 4096 May 25 15:44 Canada
-rw-r--r-- 1 root root 2094 Mar 18 01:35 CET
drwxr-xr-x 2 root root 4096 May 25 15:44 Chile
-rw-r--r-- 1 root root 2310 Mar 18 01:35 CST6CDT
-rw-r--r-- 2 root root 2416 Mar 18 01:35 Cuba
-rw-r--r-- 1 root root 1908 Mar 18 01:35 EET
-rw-r--r-- 2 root root 1955 Mar 18 01:35 Egypt
-rw-r--r-- 2 root root 3492 Mar 18 01:35 Eire
-rw-r--r-- 1 root root 114 Mar 18 01:35 EST
-rw-r--r-- 1 root root 2310 Mar 18 01:35 EST5EDT
drwxr-xr-x 2 root root 4096 May 25 15:44 Etc
drwxr-xr-x 2 root root 4096 May 25 15:44 Europe
-rw-r--r-- 1 root root 116 Mar 18 01:35 Factory
-rw-r--r-- 7 root root 3648 Mar 18 01:35 GB
-rw-r--r-- 7 root root 3648 Mar 18 01:35 GB-Eire
-rw-r--r-- 10 root root 114 Mar 18 01:35 GMT
-rw-r--r-- 10 root root 114 Mar 18 01:35 GMT+0
-rw-r--r-- 10 root root 114 Mar 18 01:35 GMT-0
-rw-r--r-- 10 root root 114 Mar 18 01:35 GMT0
-rw-r--r-- 10 root root 114 Mar 18 01:35 Greenwich
-rw-r--r-- 2 root root 1203 Mar 18 01:35 Hongkong
-rw-r--r-- 1 root root 115 Mar 18 01:35 HST
-rw-r--r-- 2 root root 1162 Mar 18 01:35 Iceland
drwxr-xr-x 2 root root 4096 May 25 15:44 Indian
-rw-r--r-- 2 root root 2582 Mar 18 01:35 Iran
-rw-r--r-- 1 root root 4463 Mar 18 01:35 iso3166.tab
-rw-r--r-- 3 root root 2388 Mar 18 01:35 Israel
-rw-r--r-- 2 root root 482 Mar 18 01:35 Jamaica
-rw-r--r-- 2 root root 309 Mar 18 01:35 Japan
-rw-r--r-- 2 root root 316 Mar 18 01:35 Kwajalein
-rw-r--r-- 1 root root 3392 Mar 18 01:35 leapseconds
-rw-r--r-- 1 root root 10666 Mar 18 01:35 leap-seconds.list
-rw-r--r-- 2 root root 625 Mar 18 01:35 Libya
-rw-r--r-- 1 root root 2094 Mar 18 01:35 MET
drwxr-xr-x 2 root root 4096 May 25 15:44 Mexico
-rw-r--r-- 1 root root 114 Mar 18 01:35 MST
-rw-r--r-- 1 root root 2310 Mar 18 01:35 MST7MDT
-rw-r--r-- 4 root root 2444 Mar 18 01:35 Navajo
-rw-r--r-- 4 root root 2437 Mar 18 01:35 NZ
-rw-r--r-- 2 root root 2068 Mar 18 01:35 NZ-CHAT
drwxr-xr-x 2 root root 4096 May 25 15:44 Pacific
-rw-r--r-- 2 root root 2654 Mar 18 01:35 Poland
-rw-r--r-- 2 root root 3497 Mar 18 01:35 Portugal
drwxr-xr-x 18 root root 4096 May 25 15:44 posix
-rw-r--r-- 3 root root 3536 Mar 18 01:35 posixrules
-rw-r--r-- 5 root root 561 Mar 18 01:35 PRC
-rw-r--r-- 1 root root 2310 Mar 18 01:35 PST8PDT
drwxr-xr-x 18 root root 4096 May 25 15:44 right
-rw-r--r-- 2 root root 761 Mar 18 01:35 ROC
-rw-r--r-- 2 root root 617 Mar 18 01:35 ROK
-rw-r--r-- 1 root root 773 Mar 18 01:35 SECURITY
-rw-r--r-- 2 root root 383 Mar 18 01:35 Singapore
-rw-r--r-- 3 root root 1947 Mar 18 01:35 Turkey
-rw-r--r-- 1 root root 112785 Mar 18 01:35 tzdata.zi
-rw-r--r-- 8 root root 114 Mar 18 01:35 UCT
-rw-r--r-- 8 root root 114 Mar 18 01:35 Universal
drwxr-xr-x 2 root root 4096 May 25 15:44 US
-rw-r--r-- 8 root root 114 Mar 18 01:35 UTC
-rw-r--r-- 1 root root 1905 Mar 18 01:35 WET
-rw-r--r-- 2 root root 1535 Mar 18 01:35 W-SU
-rw-r--r-- 1 root root 17593 Mar 18 01:35 zone1970.tab
-rw-r--r-- 1 root root 19419 Mar 18 01:35 zone.tab
-rw-r--r-- 8 root root 114 Mar 18 01:35 Zulu
On macOS:
# ls -al /usr/share/zoneinfo
lrwxr-xr-x 1 root wheel 25 Jul 14 15:48 /usr/share/zoneinfo -> /var/db/timezone/zoneinfo
# ls -al /var/db/timezone/zoneinfo
lrwxr-xr-x 1 root wheel 38 Jul 21 15:21 /var/db/timezone/zoneinfo -> /var/db/timezone/tz/2022a.1.0/zoneinfo
# ls -al /var/db/timezone/tz/2022a.1.0/zoneinfo
total 472
-rw-r--r-- 1 root wheel 6 Mar 18 08:37 +VERSION
drwxr-xr-x 68 root wheel 2176 Mar 23 10:39 .
drwxr-xr-x 5 root wheel 160 Mar 23 10:39 ..
drwxr-xr-x 56 root wheel 1792 Mar 23 10:39 Africa
drwxr-xr-x 147 root wheel 4704 Mar 23 10:39 America
drwxr-xr-x 14 root wheel 448 Mar 23 10:39 Antarctica
drwxr-xr-x 3 root wheel 96 Mar 23 10:39 Arctic
drwxr-xr-x 101 root wheel 3232 Mar 23 10:39 Asia
drwxr-xr-x 14 root wheel 448 Mar 23 10:39 Atlantic
drwxr-xr-x 25 root wheel 800 Mar 23 10:39 Australia
drwxr-xr-x 6 root wheel 192 Mar 23 10:39 Brazil
-rw-r--r-- 1 root wheel 2102 Mar 18 08:37 CET
-rw-r--r-- 1 root wheel 2294 Mar 18 08:37 CST6CDT
drwxr-xr-x 10 root wheel 320 Mar 23 10:39 Canada
drwxr-xr-x 4 root wheel 128 Mar 23 10:39 Chile
-rw-r--r-- 1 root wheel 2411 Mar 18 08:37 Cuba
-rw-r--r-- 1 root wheel 1876 Mar 18 08:37 EET
-rw-r--r-- 1 root wheel 118 Mar 18 08:37 EST
-rw-r--r-- 1 root wheel 2294 Mar 18 08:37 EST5EDT
-rw-r--r-- 1 root wheel 1946 Mar 18 08:37 Egypt
-rw-r--r-- 1 root wheel 3517 Mar 18 08:37 Eire
drwxr-xr-x 37 root wheel 1184 Mar 23 10:39 Etc
drwxr-xr-x 65 root wheel 2080 Mar 23 10:39 Europe
-rw-r--r-- 1 root wheel 120 Mar 18 08:37 Factory
-rw-r--r-- 1 root wheel 3661 Mar 18 08:37 GB
-rw-r--r-- 1 root wheel 3661 Mar 18 08:37 GB-Eire
-rw-r--r-- 1 root wheel 118 Mar 18 08:37 GMT
-rw-r--r-- 1 root wheel 118 Mar 18 08:37 GMT+0
-rw-r--r-- 1 root wheel 118 Mar 18 08:37 GMT-0
-rw-r--r-- 1 root wheel 118 Mar 18 08:37 GMT0
-rw-r--r-- 1 root wheel 118 Mar 18 08:37 Greenwich
-rw-r--r-- 1 root wheel 119 Mar 18 08:37 HST
-rw-r--r-- 1 root wheel 1217 Mar 18 08:37 Hongkong
-rw-r--r-- 1 root wheel 1174 Mar 18 08:37 Iceland
drwxr-xr-x 13 root wheel 416 Mar 23 10:39 Indian
-rw-r--r-- 1 root wheel 9776 Mar 18 08:37 Iran
-rw-r--r-- 1 root wheel 9113 Mar 18 08:37 Israel
-rw-r--r-- 1 root wheel 481 Mar 18 08:37 Jamaica
-rw-r--r-- 1 root wheel 292 Mar 18 08:37 Japan
-rw-r--r-- 1 root wheel 309 Mar 18 08:37 Kwajalein
-rw-r--r-- 1 root wheel 641 Mar 18 08:37 Libya
-rw-r--r-- 1 root wheel 2102 Mar 18 08:37 MET
-rw-r--r-- 1 root wheel 118 Mar 18 08:37 MST
-rw-r--r-- 1 root wheel 2294 Mar 18 08:37 MST7MDT
drwxr-xr-x 5 root wheel 160 Mar 23 10:39 Mexico
-rw-r--r-- 1 root wheel 2434 Mar 18 08:37 NZ
-rw-r--r-- 1 root wheel 2047 Mar 18 08:37 NZ-CHAT
-rw-r--r-- 1 root wheel 2427 Mar 18 08:37 Navajo
-rw-r--r-- 1 root wheel 556 Mar 18 08:37 PRC
-rw-r--r-- 1 root wheel 2294 Mar 18 08:37 PST8PDT
drwxr-xr-x 46 root wheel 1472 Mar 23 10:39 Pacific
-rw-r--r-- 1 root wheel 2679 Mar 18 08:37 Poland
-rw-r--r-- 1 root wheel 3483 Mar 18 08:37 Portugal
-rw-r--r-- 1 root wheel 764 Mar 18 08:37 ROC
-rw-r--r-- 1 root wheel 645 Mar 18 08:37 ROK
-rw-r--r-- 1 root wheel 384 Mar 18 08:37 Singapore
-rw-r--r-- 1 root wheel 1930 Mar 18 08:37 Turkey
-rw-r--r-- 1 root wheel 118 Mar 18 08:37 UCT
drwxr-xr-x 14 root wheel 448 Mar 23 10:39 US
-rw-r--r-- 1 root wheel 118 Mar 18 08:37 UTC
-rw-r--r-- 1 root wheel 118 Mar 18 08:37 Universal
-rw-r--r-- 1 root wheel 1518 Mar 18 08:37 W-SU
-rw-r--r-- 1 root wheel 1873 Mar 18 08:37 WET
-rw-r--r-- 1 root wheel 118 Mar 18 08:37 Zulu
-rw-r--r-- 1 root wheel 4463 Mar 18 08:37 iso3166.tab
-rw-r--r-- 1 root wheel 3392 Mar 18 08:37 leapseconds
-rw-r--r-- 1 root wheel 3519 Mar 18 08:37 posixrules
-rw-r--r-- 1 root wheel 19419 Mar 18 08:37 zone.tab
Check a timezone file type: (it is a binary file)
# file /usr/share/zoneinfo/Asia/Tokyo
/usr/share/zoneinfo/Asia/Tokyo: timezone data (fat), version 2, 4 gmt time flags, 4 std time flags, no leap seconds, 9 transition times, 4 local time types, 12 abbreviation chars
To set timezone permanently
Create a symlink from timezone file to /etc/localtime
.
rm -rf /etc/localtime
ln -sf /usr/share/zoneinfo/America/Los_Angeles /etc/localtime
With nodejs/javascript
To start node server with specific timezone setting
TZ="UTC" node app.js
To start chrome in a specific timezone setting
TZ='Asia/Bangkok' google-chrome "--user-data-dir=$HOME/chrome-profile"
To check the running environment timezone offset in Javascript
console.log(new Date().getTimezoneOffset())
# output: -540
# -540 minutes => -9 hours => UTC+9 (Asia/Tokyo)

Conversion between timezones
- Use the following utility functions
toServerDate
andtoClientDate
to handleDate
object correctly
import ms from 'ms'
const serverTimezoneOffset = -540
const clientTimezoneOffset = new Date().getTimezoneOffset()
const oneMin = ms('1 minute')
export const toServerDate = (date: Date, serverOffset?: number) => new Date(
date.getTime()
+ oneMin * (
(serverOffset ?? serverTimezoneOffset) - (date.getTimezoneOffset() ?? new Date().getTimezoneOffset())
)
)
export const toClientDate = (date: Date, serverOffset?: number) => {
const utcTime = new Date(date.getTime() - (serverOffset ?? serverTimezoneOffset) * oneMin)
return new Date(
utcTime.getUTCFullYear(),
utcTime.getUTCMonth(),
utcTime.getUTCDate(),
utcTime.getUTCHours(),
utcTime.getUTCMinutes(),
utcTime.getUTCMilliseconds()
)
}
serverTimezoneOffset
is your server fixed timezone.
Usage example:
- To display a time object received from the server. Use
toClientDate(date).toLocaleString()
. For example, server passes27/11/2018 3:00PM
in UTC+9, the client receives this date object and wants to display it exactly27/11/2018 3:00PM
in the client view. If the client just usesdate.toLocaleString()
, the printed value will be27/11/2018 1:00PM
- To convert date objected created in the client (for e.g.
27/11/2018 3:00PM
in UTC+7), and want to send to the server the data object with the same timing but in server timezone setting (i.e.27/11/2018 3:00PM
in UTC+9). UsetoServerDate(date)
Note that on the same machine, Date.getTimezoneOffset()
does NOT always return the same value when the date change.
The results are even different from browser to browser, OS to OS.
In IE, in Windows 10.

In Edge/Chrome, Windows 10.


In Chrome, windows server.

In IE, windows server.

In chrome/Arch linux:

In nodejs 15.5.1

console.log(new Date(195, 8, 2).getTimezoneOffset()) // -426
console.log(new Date(1945, 8, 1).getTimezoneOffset()) // -540
console.log(new Date(1945, 8, 2).getTimezoneOffset()) // -420
console.log(new Date(1947, 2, 31).getTimezoneOffset()) // -420
console.log(new Date(1947, 3, 1).getTimezoneOffset()) // -480
console.log(new Date(1975, 5, 12).getTimezoneOffset()) // -480
console.log(new Date(1975, 5, 13).getTimezoneOffset()) // -420
new Date(-767955600000) // Sat Sep 01 1945 00:00:00 GMT+0900 (Indochina Time)
new Date(-767869200000) // Sat Sep 01 1945 22:00:00 GMT+0700 (Indochina Time)
new Date(1945, 8, 2)
// Sun Sep 02 1945 00:00:00 GMT+0700 (Indochina Time)
new Date(1945, 8, 1)
// Sat Sep 01 1945 00:00:00 GMT+0900 (Indochina Time)
The above toServerDate
, toClientDate
are guaranteed to work in any environment.
Note 2: .setDate()
-like function does change the date object's timezone offset.
Note 3: new Date(year, month, day)
with negative values does not affect the timezone offset as if constructed with the equivalent non-negative values.

Note 4: if you want to get the label of the date related to an existing date.
In client mode (modify by displaying value): never use new Date(d.getTime() + relativeTime)
. Instead, you should create a new date and use setDate
-like functions. This way takes one more statement, but it is correct in a dynamic environment.
In server mode (absolute time, modified by the time offset): the reverse is applied. Never use setDate
-like functions.
const date = new Date(1945, 8, 1)
// Not recommended
const nextDay = new Date(date.getTime() + ms('1 day'))
// Recommended
const nextDay = new Date(date)
nextDay.setDate(nextDay.getDate() + 1)
// Wrong
const nextServerDate = toServerDate(date)
nextServerDate.setDate(nextServerDate.getDate() + 1)
// Recommended
const nextServerDate = new Date(toServerDate(date).getTime() + ms('1 day'))
TODO: In conclusion, when is the timezone database required?
