Date & time

上一篇在实作 EtaResponseMapper 的时候我们用了 Java 8 开始有的 InstantLocalDateTimeZonedDateTime。它们都是跟日期时间相关的 class。但其实 Kotlin 都有 kotlinx-datetime 做类似的东西。但目前 kotlinx-datetime 还是在早期开发阶段,有很多常用功能都未做到,例如我们这次需要用到的 formatter 目前仍需要用 Java time,所以还是用 Java time 算。

在 Java 8 之前,Java Standard Library 是有 CalendarDate 之类的 class,但它们本身的设计是古古怪怪。例如 Calendar 一月是用 0 表示,还有是没有考虑到时区问题。所以後来就出现了 Joda Time 这个 library。受到 Joda Time 的启发,之後就出了 JSR-310 的提案,最终就在 Java 8 的 Standard Library 加入了 Java Time。Java Time 除了之前用过的 DateTimeFormatterLocalDateTimeZonedDateTimeInstant 之外,还有其他常用的东西例如 ClockPeriodDuration 等等。它们都是用来表达不同的东西和方便我们计算关於日期时间的问题。例如我们会用 Instant 而不是一个 Long 的 variable 表示某个时刻、用 Duration 表示时段,这样会令 code 更易理解,不用再担心那个 Long 的单位是秒还是毫秒。另一个例子是计算 N 天後是几年几月几日就不用再刻意把时分秒清零再加天数,因为可以用 LocalDate.now().plusDays(10) 就可以了。

由於旧版 Android 并未支援 Java 8 的功能,所以我们需要靠 desugaring 来为我们的 app 补上部分 Java 8 或以上的 language feature,当中包括大部分的 Java Time 功能。加入 desugaring library 的方法很简单,就是在 app module 的 build.gradle 加上以下的东西:

android {
    // ...
    compileOptions {
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugaringVersion"
    // ...
}

Desugaring 的原理其实就是把那些 Java 8 或以上能够 backport 到的 language feature 在 compile 时帮你补上去 APK/AAB 内,所以不论 Android 版本是新还是旧都能行到那段 code,反正都是用你 APK/AAB 提供的 class 执行。

https://ithelp.ithome.com.tw/upload/images/20210924/20139666ANpqWWZCZf.png

在未有 desugering 之前,一般都会用 ThreeTen Backport 代替 Java Time。

时区问题

另一样想借本篇讨论的是时区的问题。在台湾或者香港都是 UTC+08:00,而且没有日光节约时间 (daylight saving time, DST)。所以很多时在设计系统都没有认真考虑时区问题(或者没有意识到有这个问题),全部时间储存都一概使用 UNIX timestamp。但如果系统日後需要支援多时区时就很难改得动了。

首先我们要了解 Time Zone Database 和 time zone ID。Time Zone Database 是由 IANA 管理的时区资料库(IANA 就是管理域名的那个机构),那个 database 就是要记录世界各地以前至未来已知的时区资讯,当中包括日光节约时间的切换规则。

那 time zone ID 就是我们平常写 code 看到的 Asia/Hong_KongAsia/TaipeiAsia/ShanghaiAmerica/New_York 之类的东西。这些 ID 就是找一些有代表性的地名来命名,代表性的意思是指以时区、政府、以往实行过的时区之类有独特性。虽然 Asia/Hong_KongAsia/TaipeiAsia/Shanghai 在现在都是指向 UTC+08:00,但为了能顺利地转换以前的日期时间仍要保留三个不同的 ID 表示。如果你有下载过 Time Zone Database 的话,用普通的纯文字编辑器打开就会看到它会纪录每个 time zone ID 何时选用那个 UTC 偏移量 (offset) 和有没有执行夏令时间的资讯,还有更多的是有间该 time zone ID 的相关文献。如果想了解更多那个地方时区的历史的话 Time Zone Database 是不容错过。

Time Zone Database 香港的部分

Time Zone Database 香港的部分

其中一个有趣的东西是 Time Zone Database 跟中国大陆相关的有好几个 ID,这是因为中国大陆曾经分被划分为五个时区

  • Asia/Shanghai 上海 (UTC+08:00)
  • Asia/Urumqi 乌鲁木齐 (UTC+06:00)
  • Asia/Harbin 哈尔滨,现在指向 Asia/Shanghai
  • Asia/Chongqing 重庆,现在指向 Asia/Shanghai
  • Asia/Kashgar 喀什,现在指向 Asia/Urumqi

这些重新指向的 ID 都是为了向後兼容,即是如果表达一个以前的当地时间我们仍可以配搭这些 ID 从而计算出 UNIX timestamp 或者反向计算出当地时间。

Windows 时区列表出现好几个地名反映了以前那些地方都采用不同时区

Windows 时区列表出现好几个地名反映了以前那些地方都采用不同时区

除了一般的时区问题之外,部分地方会实行日光节约时间 (Daylight Saving Time, DST)。意思是一年会切换两次时区:大约在春季左右会找一天把时间调快一小时;在秋季又会再把时间调慢一小时还原。用意是因为夏天的日照时间长,把日常活动都调快一小时就能接触更多阳光,从而节省能源(例如开少一小时电灯)。当然去到今时今日还能不能节省能源已成疑问,加上切换时间那两天对工作和生活都造成影响。所以欧盟有考虑过废除日光节约时间,但目前尚未实行。

其实写了那麽多 Time Zone Database 的东西都是想指出 UNIX timestamp 不能万能,尤其是用於表达将来的时间。因为时区可以随时因为各地政府的政策变更(例如会否在将来取消日光节约时间),所以如果要储存几年後的某月某日早上 9 时要开会的话,我们应该储存当地时间及 time zone ID,不是 UNIX timestamp 或者当地时间及 UTC 偏移量。这样即使政府改变时区都不会影响到储存的资料(因为可以靠更新 Time Zone Database 以取得正确结果)。其中一个改变时区的例子是北韩,在 2015 年 8 月 15 日至 2018 年 5 月 4 日期间改用 UTC+08:30,其後改回跟南韩一样 UTC+09:00。但如果是储存过去的日期时间的话用 UNIX timestamp 就没有大问题,因为过去的东西不会再改。

Time Zone Database 更新

IANA 会跟据各地政府对时区的变更更新 Time Zone Database,所以一年可能会发布好几个版本。这个 database 在不少地方用到,例如大家平常使用的作业系统、JRE 等等。它们都会透过系统更新或 JRE 版本更新来更新它们内里所用的 Time Zone Database。有关 Android 系统更新 database 的方法可以参考 AOSP 网站。

如果很在乎 Time Zone Database 是不是最新版的话,目前似乎只有 TickTock(不是抖音)和用回 ThreeTen Backport

另外,在使用 Java Time 的 method 前,紧记检查 desugaring 支援的 class 和对 Java Time 的特别说明,因为 backport 始终有部分的实作仍依赖原系统的实作。

参考


<<:  Day9 Goroutine

>>:  【从零开始的Swift开发心路历程-Day12】打造自己的私房美食名单Part1

Day30-2 - GitLab CI 还可以怎麽重构及整理 .gitlab-ci.yml ?

上一篇举了一个小例子来说明,一般遇到比较冗长的 .gitlab-ci.yml 大致上可以怎麽思考整理...

人脸辨识的简介

在这挑战中,想分享人脸辨识的相关基础与流程,以及谈到一些常见的模型架构。 一开始会说明人脸辨识兴起的...

#12-套件掰!用JS 做进场特效 (Intersection Observer API)

进场特效也是基本再基本的网页动态! 尤其是当网页内容塞太多时,适当地加上进场特效可以帮助使用者阅读重...

Day17-sklearn(2)LabelEncoder、train_test_split

今天要介绍这两个sklearn的方法 也是资料前处理常用到的 LabelEncoder: 就如同字面...

如何自己设计一套ERP程序 前传-写ERP之前要决定的20件事 前言

这篇文章 [如何自己设计一套ERP程序 前传-写ERP之前要决定的20件事],原本是计画去年要参加铁...