Best practices when working with universal time

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 and toClientDate to handle Date 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 passes 27/11/2018 3:00PM in UTC+9, the client receives this date object and wants to display it exactly 27/11/2018 3:00PM in the client view. If the client just uses date.toLocaleString(), the printed value will be 27/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). Use toServerDate(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?

Buy Me A Coffee