LocalDateTime ZonedDateTime System.currentTimeMillis() Date

了解Java時區的處理方式與應用

陳瑞泰 John Chen 2022/08/28 23:29:39
3935

**了解Java時區的處理方式與應用**

 

開發應用系統顯示時間是常見的需求,大概不會有應用系統沒有「顯示時間」功能,前端頁面上顯示 “年/月/日 時:分:秒” ,大概的做法是取 DB中的值直接吐出來當字串顯示,或是以 long 加上 dateFormat 物件來以格式化字串,但是這個做法若扯上「時區」的話,問題就會變得再更複雜一些。以時區來說,通常會有這幾個需求:

 

1. UI上的時間固定以 zh_TW 時區顯示,User 身在美國跟在台灣要在 UI 上看到同樣的時間

2. UI 上的時間會根據使用者的瀏覽器時區設置不同,User 身在美國跟在台灣看到的時間會不一樣

 

上述只是顯示的部分而已,另外一個部分是與後端API 的溝通,要正確處理時間跟時區並不是一件簡單的事,前端、後端與SA流程及DB欄位定義都要搭配。

 

 

Timestamp

我們先來看看 java 中的timestamp寫法,印出當前時間這個數字怎麼解讀它呢?它表示「從 UTC+0 時區的 1970 年 1 月 1 號 0 時 0 分 0 秒開始,總共過了多少ms」

java code:

System.out.println(System.currentTimeMillis());  //1658730994161

 

很明顯地,這個數字不人性化,所以一定要格式化,那麼直覺來想 ”SimpleDateFormat” 它就可以顯示yyyy-MM-dd hh:mm:ss了,只是這樣的字串我們把它帶到上述的情境中,我在美國看到 yyyy-MM-dd hh:mm:ss 和在台灣看到 yyyy-MM-dd hh:mm:ss 是一樣的意思嗎? 顯然不是,那有沒有一種全球統一的表示方法呢?有的...它就是 UTC 格式或是說它是 ISO 8601格式,它被定義在 “RFC 3339” 文件中,主要是上述的文字格式中加入了 “時區” 概念,基本的格式長這樣:

2020-12-26T12:38:00Z

 

    1. T 就代表 full date-time

    2. Z 就代表 UTC +0,格林威治時間

    3. 如果要其他時區可以這樣寫:2022-07-25T23:59:59+08:00,代表 +8 時區或是台北,格林威治時間+8小時

 

因此2022-07-25T23:59:59+08:00 等於 2022-07-25T15:59:59+00:00 雖然時間不一樣,但是加了時區這二組就表示同一個時間因為時區是相對概念,一切以UTC為基準。那麼我們來想一個實際的問題,例如:有一個 long 的時間值加上系統預設時區,那麼我們如何來做資料的呈現呢?

java code:

long nowlong = System.currentTimeMillis();  // 我的電腦現是 2022-07-28 17:03:54

System.out.println("取出 long:\t" + nowlong);

System.out.println("......Long 轉 LocalDateTime");

System.out.println("系統預設時區 : " + ZoneId.systemDefault());

LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(nowlong), ZoneId.systemDefault());

System.out.println("時區 - 日期時間格式如下:");

System.out.println(localDateTime.atZone(ZoneId.systemDefault()));  //記得指定時區

System.out.println(localDateTime.atZone(ZoneId.of("UTC"))); //指定時區, ex: Asia/Taipei

System.out.println(localDateTime); //未指定

output:

取出 long: 1658999034770

......Long 轉 LocalDateTime

系統預設時區 : Asia/Taipei

時區 - 日期時間格式如下:

2022-07-28T17:03:54.770+08:00[Asia/Taipei]

2022-07-28T17:03:54.770Z[UTC]

2022-07-28T17:03:54.770

由程式碼結果來看,加上時區似乎只是在字串中加入時區的字串標記,所以使用 localDateTime.atZone(...) ,只能是格式化字串,沒有辦法移到真正的時區時間,我們比較想要取「現在台北時間」後轉成「UTC時間」那要如何操作呢?答案是使用ZoneDateTime物件,顧名思義它已加上了Zone概念,所以並不是單純的Local時區,請看以下程式範例:

 

java code:

…. 承上....

ZonedDateTime zonedSys = localDateTime.atZone(ZoneId.systemDefault()); // localDateTime 指定 ZoneId 後變成 ZoneDateTime 就可位移

ZonedDateTime utcZone = zonedSys.withZoneSameInstant(ZoneId.of("UTC")); //ZoneDateTime 位移 到 UTC

System.out.println("位移 到 UTC = " + utcZone);

System.out.println("位移 到 UTC, 轉為 long = " + utcZone.toInstant().toEpochMilli()); // long

output:

取出 long: 1659000584080

......Long 轉 LocalDateTime

系統預設時區 : Asia/Taipei

時區 - 日期時間格式如下:

2022-07-28T17:29:44.080+08:00[Asia/Taipei]

2022-07-28T17:29:44.080Z[UTC]

2022-07-28T17:29:44.080

位移 到 UTC = 2022-07-28T09:29:44.080Z[UTC]

位移 到 UTC, 轉為 long = 1659000584080

使用ZoneDateTime 位移 到 UTC 後,觀察它們的小時部分,發現UTC 真的少了8小時,且由 ZoneDateTime轉出的 long 值與 System.currentTimeMillis() 一模一樣,由此可知 System.currentTimeMillis() 就是 UTC 的 long 值,這點與 Ttimestamp 的定義相同。所以說若我們的系統想要記錄統一時間,可以直接記錄 long 這個長整數,之後再由格式轉換來顯示及呈現即可,那麼接下來就是「LocalDateTime、ZoneDateTime、Long、String 相互轉換」要處理的問題了。

 

java code:

DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd");


LocalDateTime localDateTime = LocalDateTime.parse("2019-07-31 00:00:00",dateTimeFormatter1);

LocalDate localDate = LocalDate.parse("2019-07-31",dateTimeFormatter2);

Date date = Date.from(LocalDateTime.parse("2019-07-31 00:00:00",dateTimeFormatter1).atZone(ZoneId.systemDefault()).toInstant());



String strDateTime = "2019-07-31 00:00:00";

String strDate = "2019-07-31";

Long timestamp=1564502400000l;


/** LocalDateTime 轉 LocalDate */

System.out.println("LocalDateTime 轉 LocalDate: "+localDateTime.toLocalDate());

/** LocalDateTime 轉 Long */

System.out.println("LocalDateTime 轉 Long: "+localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());

/** LocalDateTime 轉 Date */

System.out.println("LocalDateTime 轉 Date: "+Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()));

/** LocalDateTime 轉 String */

System.out.println("LocalDateTime 轉 String: "+localDateTime.format(dateTimeFormatter1));


System.out.println("-------------------------------");


/** LocalDate 轉 LocalDateTime */

System.out.println("LocalDate 轉 LocalDateTime: "+LocalDateTime.of(localDate,LocalTime.parse("00:00:00")));

/** LocalDate 轉 Long */

System.out.println("LocalDate 轉 Long: "+localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli());

/** LocalDate 轉 Date */

System.out.println("LocalDate 轉 Date: "+Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()));

/** LocalDate 轉 String */

System.out.println("LocalDateTime 轉 String: "+localDateTime.format(dateTimeFormatter2));


System.out.println("-------------------------------");


/** Date 轉 LocalDateTime */

System.out.println("Date 轉 LocalDateTime: "+LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()));

/** Date 轉 Long */

System.out.println("Date 轉 Long: "+date.getTime());

/** Date 轉 LocalDate */

System.out.println("Date 轉 LocalDateTime: "+LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()).toLocalDate());

/** Date 轉 String */

SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS" );

System.out.println("Date 轉 String: "+sdf.format(date));


System.out.println("-------------------------------");


/** String 轉 LocalDateTime */

System.out.println("String 轉 LocalDateTime: "+LocalDateTime.parse(strDateTime,dateTimeFormatter1));

/** String 轉 LocalDate */

System.out.println("String 轉 LocalDate: "+LocalDateTime.parse(strDateTime,dateTimeFormatter1).toLocalDate());

System.out.println("String 轉 LocalDate: "+LocalDate.parse(strDate,dateTimeFormatter2));

/** String 轉 Date */

System.out.println("String 轉 Date: "+Date.from(LocalDateTime.parse(strDateTime,dateTimeFormatter1).atZone(ZoneId.systemDefault()).toInstant()));


System.out.println("-------------------------------");


/** Long 轉 LocalDateTime */

System.out.println("Long 轉 LocalDateTime:"+LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()));

/** Long 轉 LocalDate */

System.out.println("Long 轉 LocalDate:"+LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()).toLocalDate());	

/** Long 轉 ZoneDateTime */

localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());

System.out.println("Long 轉 ZoneDateTime:"+ ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); // 可給 JS 使用

System.out.println("Long 轉 ZoneDateTime:"+ ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).format(DateTimeFormatter.ISO_ZONED_DATE_TIME));

ZonedDateTime utcZone = ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).withZoneSameInstant(ZoneId.of("UTC")); //ZoneDateTime 位移 到 UTC

System.out.println("Long 轉 ZoneDateTime:"+ utcZone.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); // 可給 JS 使用

System.out.println("Long 轉 ZoneDateTime:"+ utcZone.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));


System.out.println("-------------------------------");

System.out.println("ms = 123" );

timestamp += 123;

localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());

System.out.println("Long 轉 ZoneDateTime:"+ ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); // 可給 JS 使用

System.out.println("Long 轉 ZoneDateTime:"+ ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).format(DateTimeFormatter.ISO_ZONED_DATE_TIME));

utcZone = ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).withZoneSameInstant(ZoneId.of("UTC")); //ZoneDateTime 位移 到 UTC

System.out.println("Long 轉 ZoneDateTime:"+ utcZone.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); // 可給 JS 使用

System.out.println("Long 轉 ZoneDateTime:"+ utcZone.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));

System.out.println("-------------------------------");

System.out.println("Long 轉 ZoneDateTime (JS可用) :"+ ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); // 可給 JS 使用

System.out.println("Long 轉 ZoneDateTime (JS可用) :"+ utcZone.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));// 可給 JS 使用


System.out.println("-------------------------------");


/** ISO 8601 String 轉 ZoneDateTime */

strDateTime = "2022-07-28T05:59:59.000+08:00";

System.out.println("...標準格式解析【不用formatter】 : " + strDateTime);

System.out.println("String 轉 ZonedDate: "+ZonedDateTime.parse(strDateTime));

strDateTime = "2022-07-28T05:59:59.000Z";

System.out.println("...標準格式解析【不用formatter】 : " + strDateTime);

System.out.println("String 轉 ZonedDate: "+ZonedDateTime.parse(strDateTime));

strDateTime = "20220728T000000Z";

System.out.println("...自訂格式解析 'Z' : " + strDateTime);

DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssz");

System.out.println("String 轉 ZonedDate: "+ZonedDateTime.parse(strDateTime, formatter1));		

strDateTime = "20220728T000000+0000";

System.out.println("...自訂格式解析 +0800 : " + strDateTime);

DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssZ");

System.out.println("String 轉 ZonedDate: "+ZonedDateTime.parse(strDateTime, formatter2));

output:

LocalDateTime 轉 LocalDate: 2022-07-28
LocalDateTime 轉 Long: 1658959199000
LocalDateTime 轉 Date: Thu Jul 28 05:59:59 CST 2022
LocalDateTime 轉 String: 2022-07-28 05:59:59
-------------------------------
LocalDate 轉 LocalDateTime: 2022-07-28T00:00
LocalDate 轉 Long: 1658937600000
LocalDate 轉 Date: Thu Jul 28 00:00:00 CST 2022
LocalDateTime 轉 String: 2022-07-28
-------------------------------
Date 轉 LocalDateTime: 2022-07-28T05:59:59
Date 轉 Long: 1658959199000
Date 轉 LocalDateTime: 2022-07-28
Date 轉 String: 2022-07-28 05:59:59 000
-------------------------------
String 轉 LocalDateTime: 2022-07-28T05:59:59
String 轉 LocalDate: 2022-07-28
String 轉 LocalDate: 2022-07-28
String 轉 Date: Thu Jul 28 05:59:59 CST 2022
-------------------------------
Long 轉 LocalDateTime:2022-07-28T05:59:59
Long 轉 LocalDate:2022-07-28
Long 轉 ZoneDateTime:2022-07-28T05:59:59+08:00
Long 轉 ZoneDateTime:2022-07-28T05:59:59+08:00[Asia/Taipei]
Long 轉 ZoneDateTime:2022-07-27T21:59:59Z
Long 轉 ZoneDateTime:2022-07-27T21:59:59Z[UTC]
-------------------------------
ms = 123
Long 轉 ZoneDateTime:2022-07-28T05:59:59.123+08:00
Long 轉 ZoneDateTime:2022-07-28T05:59:59.123+08:00[Asia/Taipei]
Long 轉 ZoneDateTime:2022-07-27T21:59:59.123Z
Long 轉 ZoneDateTime:2022-07-27T21:59:59.123Z[UTC]
-------------------------------
Long 轉 ZoneDateTime (JS可用) :2022-07-28T05:59:59.123+08:00
Long 轉 ZoneDateTime (JS可用) :2022-07-27T21:59:59.123Z
-------------------------------
...標準格式解析【不用formatter】 : 2022-07-28T05:59:59.000+08:00
String 轉 ZonedDate: 2022-07-28T05:59:59+08:00
...標準格式解析【不用formatter】 : 2022-07-28T05:59:59.000Z
String 轉 ZonedDate: 2022-07-28T05:59:59Z
...自訂格式解析 'Z' : 20220728T000000Z
String 轉 ZonedDate: 2022-07-28T00:00Z
...自訂格式解析 +0800 : 20220728T000000+0000
String 轉 ZonedDate: 2022-07-28T00:00Z



API 回傳顯示問題

假設前端單純只是顯示API時間,那麼前端只要在call API 時傳入locale值(Ex:zh_TW),那麼後端就可以利用 timestamp(long) + locale(zh_TW) 計算出前端頁面所需要的字串值嗎?答案是:不能。因為沒有“time zone to locale”之類的東西。那是因為有些個國家擁有許多個時區(例如美國)。 

 

時區是一個相對UTC的概念。需要考慮實際的應用情況才能決定實作做方法,假如我是一個旅客,預計要搭台灣高鐵 2022-7-30,下午1:30前往左營的列車。那麼 API 回傳給 Browser 必需使用標準ISO格式 2022-07-30T13:30:00+08:00,而 Browser 必需轉換為人類習慣的格式,ex: '2022-07-30 13:30' ,這裡小弟推薦使用 JavaScript(https://day.js.org/) Day.js Timezone 外掛套件,此套件語法如下:dayjs('2022-07-30T13:30:00+08:00').format('HH:mm') 時,若我是台彎人那不會有問題,因為它預設會使用設備的 Timezone,可以用它來查詢 console.log('目前時區 => ' + dayjs.tz.guess()),所以時間會是正常的 '13:30',但若我是日本人那麼出現的時間就會是 '14:30',因為日本時區比台灣多了一小時,所以前端在撰寫時要採用顯式的指定時區 'Asia/Taipei' or '+08:00'。例如: 

  dayjs('2022-07-30T13:30:00+08:00')

  .tz('Asia/Taipei')

  .format('HH:mm:ss')

       

API to JavaScript 程式如下:

Java Code:

long timestamp = 1659159000000L; //2022-07-30T13:30:00.000+08:00
System.out.println("API to JavaScript");
/** Long 轉 ZoneDateTime */
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
System.out.println("Long 轉 ZoneDateTime:"+ ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
System.out.println();
System.out.println("...以下可傳給 JavaScript");
System.out.println("Long 轉 ZoneDateTime:"+ ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); // 可給 JS 使用,2022-07-30T13:30:00+08:00
System.out.println();
ZonedDateTime utcZone = ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).withZoneSameInstant(ZoneId.of("UTC")); //ZoneDateTime 位移 到 UTC
System.out.println("...以下可傳給 JavaScript");
System.out.println("Long 轉 ZoneDateTime:"+ utcZone.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); // 可給 JS 使用,2022-07-30T05:30:00Z
System.out.println();
System.out.println("系統預設時區 : " + ZoneId.systemDefault());
API to JavaScript
Long 轉 ZoneDateTime:2022-07-30T13:30:00+08:00[Asia/Taipei]

...以下可傳給 JavaScript
Long 轉 ZoneDateTime:2022-07-30T13:30:00+08:00

...以下可傳給 JavaScript
Long 轉 ZoneDateTime:2022-07-30T05:30:00Z

系統預設時區 : Asia/Taipei

JavaScript Code 使用線上editor

https://playcode.io/javascript/ => / index.html

    <script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.1/dayjs.min.js"></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.1/plugin/utc.min.js"></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.1/plugin/timezone.min.js"></script>

    <script>

        dayjs.extend(window.dayjs_plugin_utc);

        dayjs.extend(window.dayjs_plugin_timezone);

    </script>

  <body>

    <h1 id="header"></h1>

    <script src="src/script.js"></script>

  </body>

https://playcode.io/javascript/ => / script.js

console.log(

  '收到 API 回傳UTC (2022-07-30T05:30:00Z)   一定要顯示設定 tz(), 才能保證訂票時間為台灣高鐵搭車時間 => '+

  dayjs('2022-07-30T05:30:00Z')

  .tz('Asia/Taipei')

  .format('HH:mm:ss')

)



console.log(

  '收到 API 回傳 (2022-07-30T13:30:00+08:00) 一定要顯示設定 tz(), 才能保證訂票時間為台灣高鐵搭車時間 => '+

  dayjs('2022-07-30T13:30:00+08:00')

  .tz('Asia/Taipei')

  .format('HH:mm:ss')

)

CONSONE:

收到 API 回傳UTC (2022-07-30T05:30:00Z)   一定要顯示設定 tz(), 才能保證訂票時間為台灣高鐵搭車時間 => 13:30:00

收到 API 回傳 (2022-07-30T13:30:00+08:00) 一定要顯示設定 tz(), 才能保證訂票時間為台灣高鐵搭車時間 => 13:30:00



 

 

 

JS 傳送時間給後端API

承上搭台灣高鐵範例,前端收到 UTC 格式 (2022-07-30T05:30:00Z),而旅客看到 (13:30:00) 並且下了訂單,此時JavaScript 應回傳給 Java以下四種格式, Java 收到資料後轉換為 Long 的方法:

UTC

2022-07-30T05:30:00Z

String strDateTime = "2022-07-30T05:30:00Z";

ZonedDateTime.parse(strDateTime).toInstant().toEpochMilli();

//1659159000000

Asia/Taipei

2022-07-30T13:30:00+08:00

ZonedDateTime.parse(strDateTime).toInstant().toEpochMilli();

//1659159000000

timestamp

1659159000000

不用轉

無時區

2022-07-30  13:30:00 【千萬不要傳這種】

【千萬不要傳這種】

 

 

資料儲存方式

直接存入 long 值或是timestamp,這方式不會有任何 storage的限制,不論是RDB/NoSQL/File/JSON….之類的,都不會有問題,只是人類不好看,但效率高且可進行大小於的比較判斷。如果一定要有好看的格式建議使用位移法生成字串後再另存。

 

Java Code:

LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1659159000000), ZoneId.systemDefault());

ZonedDateTime utcZone = ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).withZoneSameInstant(ZoneId.of("UTC")); //ZoneDateTime 位移 到 UTC

System.out.println("Long 轉 ZoneDateTime:"+ utcZone.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME).toString()); // 可給 JS 使用,2022-07-30T05:30:00Z

 

 

結論

不同的程式語言框架大部分都有提供轉換的方法,所以千萬不要自已人工的+8小時去動手做,那會是災難的開始,而言以上就是由前端到後端的時區統一處理方法,本文最後附上 Java 可用的 TimeZoneUtil.java 希望您會喜歡。

package tpi.dgrv4.codec.utils;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class TimeZoneUtil {
	public static String long2UTCstring(long timestamp) {
		LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.of("UTC"));
		ZonedDateTime utcZone = ZonedDateTime.of(localDateTime, ZoneId.of("UTC"));
		String str = utcZone.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); 
		return str;
	}
	
	public static String long2AnyISO8601string(long timestamp, ZoneId zoneid) {
		LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.of("UTC"));
		ZonedDateTime utcZone = ZonedDateTime.of(localDateTime, ZoneId.of("UTC")).withZoneSameInstant(zoneid);
		String str = utcZone.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); 
		return str;
	}
	
	public static long utc2long(String UTCString) {
		ZonedDateTime x = ZonedDateTime.parse(UTCString);
		return x.toInstant().toEpochMilli();
	}
	
	public static long anyISO86012long(String ISOString) {
		return utc2long(ISOString);
	}
}

 

Reference:

1. https://blog.techbridge.cc/2020/12/26/javascript-date-time-and-timezone/

2. https://datatracker.ietf.org/doc/html/rfc3339

3. https://www.cnblogs.com/puke/p/11314431.html

4. https://lokalise.com/blog/date-time-localization/

5. https://playcode.io/javascript/

6. https://day.js.org/docs/en/timezone/timezone

陳瑞泰 John Chen