POSIX-1.2024でのタイムゾーン環境変数TZ定義の変更点と処理系の比較

# POSIX-1.2024でのTZ定義の変更点

2024年6月 POSIX-1.2024 が発行された。タイムゾーン環境変数TZのPOSIX定義について、旧版の[POSIX-1.2017](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_03) から [POSIX-1.2024](https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap08.html#tag_08_03) への変更点、およびそれに関するいくつかの処理系の挙動について述べる。

調査対象とする処理系は、特に言及がある場合を除いて、以下の通り:
- glibc 2.40 (Fedora Linux 42/rawhideを利用)
- FreeBSD 14.1 libc
- macOS 15.0 libc
- musl 1.2.5 (Alpine Linux 3.20.3を利用)

## 変更点1. IANA Time Zone DatabaseのゾーンID指定 が標準化された

`TZ=Asia/Tokyo` や `TZ=America/New_York` のような形式の、地理ベースのタイムゾーン指定がPOSIX-1.2024で標準化された。(以下、この形式を「地理的タイムゾーン形式」(geographical timezone[^timezone])と呼ぶ)。

[^timezone]: 余談であるが、POSIX-1.2024はTZの説明において"timezone"と"time zone"と表記に揺れがある。ともに一般名詞として使用されており、表記の違いはおそらく意図したものではないだろう。POSIX-1.2017時点では"timezone"で統一されている。

POSIX-1.2024発行前から、地理的タイムゾーン形式は、[IANA Time Zone Database](https://www.iana.org/time-zones) (以下tzdata) で提供されるゾーンファイルを参照する方法として、ほとんどの主要な処理系が対応しており、デファクトスタンダードとして広く普及していたが、POSIX-1.2017以前では一切言及のない処理系独自拡張であった。POSIX-1.2024でこれが標準化された。

地理的タイムゾーン形式の具体的な書式および、その値が示す参照先のデータソース (タイムゾーンデータベース) は、処理系定義 (implementation-defined) とされている。データソースとしてのtzdataおよびそのゾーンIDの一般書式である "Area/Location" は、典型例としてPOSIXでも明示されているが、これに限るものではない。ただし、処理系定義のデータソースは以下の情報を提供しなければならない:

* 標準時のUTCオフセット
* DST (いわゆる夏時間) 定義がある場合、標準時←→DST切り替わりルール (切り替わり日時の判定方法) と、DSTのUTCオフセット (なお、DST定義は過去、現在、未来を問わない)
* 標準時とDST (あれば) の名称。

IANA tzdataを用いる場合は、コンパイル済みデータ形式は [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536) に基づき、上記の情報が提供される。

なお、地理的タイムゾーン形式は、POSIX上は正確には「A format specifying a geographical timezone or a special timezone」と記載されている。tzdataは、"Etc/UTC"のように特定地域に依存しないゾーンファイルも提供しており、これらを含めて「special timezone」と表現しているものと考えられる。もし純粋に地理ベースのものに限定してしまうと、UTCなどが指定できなくなってしまう。この形式は処理系定義なので、地理ベース以外のものを提供することを明示的に許容している。

環境変数 `TZ` の指定方法とは少し次元の異なる話ではあるが、POSIX-1.2024では、TZ定義の中でタイムゾーンデータベースの更新手段についても言及がある。IANA tzdataのような外部データベースを用いる場合、処理系はその更新手段を提供しなければならない、としている。具体的な更新方法は処理系定義である。タイムゾーン情報の更新は、一般的に実世界の国や地域の時間制度の変更に追従する形で行われる (もちろん過去データの不具合修正もある)。IANA tzdataも、年に数回アップストリームから更新版がリリースされている (tzdataのメンテナンスは[RFC 6557](https://datatracker.ietf.org/doc/html/rfc6557)で定められている)。ダウンストリームたる各OSはアップストリームの更新版tzdataをコンパイル、パッケージングして、ユーザーに提供している。(多くのLinuxディストリビューションでは `tzdata` というパッケージ名で提供されている)。

### 処理系の挙動

※以降、各処理系の`TZ` ハンドリング確認は、シェルユーティリティの `date` コマンドを用いて行う。GNUスタイルの`date`とBSDスタイルの`date`はコマンドラインオプションが大きく異なる。以降のコマンドサンプルはできるだけ似た形になるようにしたがOSによって使用する実行コマンドが異なる。


各処理系のマニュアルは以下を参照のこと。
- [glibc](https://sourceware.org/glibc/manual/2.40/html_node/Geographical-TZ.html)
- [FreeBSD](https://man.freebsd.org/cgi/man.cgi?query=tzset)
- [macOS](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/tzset.3.html)
- [musl](https://wiki.musl-libc.org/environment-variables.html#%3Ccode%3ETZ%3C/code%3E)

#### glibc

```console:glibc
# 日本時間
$ TZ=Asia/Tokyo date -d @1704067200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-01-01T09:00:00+0900[JST]

# 絶対パス
$ TZ=/usr/share/zoneinfo/Asia/Tokyo date -d @1704067200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-01-01T09:00:00+0900[JST]

# 米東部時間 標準時
$ TZ=America/New_York date -d @1704067200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2023-12-31T19:00:00-0500[EST]

# 米東部時間 DST
$ TZ=America/New_York date -d @1719792000 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-06-30T20:00:00-0400[EDT]
```

#### FreeBSD

```console:FreeBSD
# 日本時間
$ TZ=Asia/Tokyo date -r 1704067200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-01-01T09:00:00+0900[JST]

# 絶対パス
$ TZ=/usr/share/zoneinfo/Asia/Tokyo date -r 1704067200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-01-01T09:00:00+0900[JST]

# 米東部時間 標準時
$ TZ=America/New_York date -r 1704067200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2023-12-31T19:00:00-0500[EST]

# 米東部時間 DST
$ TZ=America/New_York date -r 1719792000 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-06-30T20:00:00-0400[EDT]
```

#### macOS

```console:macOS
# 日本時間
$ TZ=Asia/Tokyo date -r 1704067200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-01-01T09:00:00+0900[JST]

# 絶対パス 
$ TZ=/usr/share/zoneinfo/Asia/Tokyo date -r 1704067200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-01-01T09:00:00+0900[JST]

# 米東部時間 標準時
$ TZ=America/New_York date -r 1704067200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2023-12-31T19:00:00-0500[EST]

# 米東部時間 DST
$ TZ=America/New_York date -r 1719792000 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-06-30T20:00:00-0400[EDT]
```

#### musl

```console:musl
# 日本時間
$ TZ=Asia/Tokyo date -d @1704067200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-01-01T09:00:00+0900[JST]

# 絶対パス
$ TZ=/usr/share/zoneinfo/Asia/Tokyo date -d @1704067200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-01-01T09:00:00+0900[JST]

# 米東部時間 標準時
$ TZ=America/New_York date -d @1704067200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2023-12-31T19:00:00-0500[EST]

# 米東部時間 DST
$ TZ=America/New_York date -d @1719792000 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-06-30T20:00:00-0400[EDT]
```

### 補足 POSIX-1.2017以前のTZ規定

地理的タイムゾーン形式規定の無いPOSIX-1.2017以前は、TZ値の指定方法として、以下の二つの形式を定めていた。

* `:` (コロン) で始まる処理系定義の形式 (以下「コロン形式」と呼ぶ)

    これは、例えば `TZ=:Asia/Tokyo` のような形式である。コロンで始まる場合、その解釈は処理系定義とすることがPOSIXで明示的に規定されている。多くの処理系が、コロンに続いてtzdataゾーンIDを指定する形で、tzdataのゾーンファイルを参照する機能を提供している。さらに各処理系は、この延長として、コロン無しでも同様にtzdataを参照する機能を独自拡張として以前から実装していた。つまり、多くの処理系において、POSIX-1.2024登場以前から、`TZ=:Asia/Tokyo` と `TZ=Asia/Tokyo` を同じように扱っている。

* `std offset[dst[offset][,rule]]` の書式を取るPOSIXタイムゾーン形式 (以下、この形式を「POSIXタイムゾーン形式」あるいは単に「POSIX形式」と呼ぶ)

    POSIXタイムゾーン形式は、TZ値としてPOSIX内で具体的なシンタックス、セマンティクスが規定されている唯一の形式である。詳細は、記事「[POSIXタイムゾーン形式解説](https://qiita.com/jmatsuzawa/items/35fcbb8da38af0bbbcd1)」を参照のこと。


### 各形式の優先順位

一つのTZ値が、地理的タイムゾーン形式としても、POSIXタイムゾーン形式としても、どちらでも妥当な場合どちらを優先して解釈すべきか。そのような値の例として、`TZ=EST5EDT` がある。

:::note info
tzdataは後方互換性のために `/usr/share/zoneinfo/EST5EDT` を提供してきた。その内容は、1966年制定の米統一時間法 ([Uniform Time Act](https://en.wikipedia.org/wiki/Uniform_Time_Act)) 以前の一部のタイムゾーン情報を除いて `/usr/share/zoneinfo/America/New_York` と同一である。なお、2024年9月リリースの[tzdata 2024b](https://lists.iana.org/hyperkitty/list/tz-announce@iana.org/thread/IZ7AO6WRE3W3TWBL5IR6PMQUL433BQIE/)からは、`/usr/share/zoneinfo/EST5EDT` は`/usr/share/zoneinfo/America/New_York` への[リンクとして提供](https://github.com/eggert/tz/commit/a0b09c0230089252acf2eb0f1ba922e99f7f4a03#diff-47823b565d4a696e1db5f479faca0aa271899c05d24971730015cb294d0b6446R65)される。従来通り独立したファイルとして利用することも可能。
:::

以下で具体的に確認するが、glibc, FreeBSD, macOS は優先的に地理的タイムゾーン形式として解釈し、その値のゾーンIDが存在しない場合はPOSIX形式として解釈する。一方、muslは優先的にPOSIXタイムゾーン形式として解釈する。

POSIX-1.2024は、優先順位について `If TZ is of the third format (that is, if the first character is not a <colon> and the value does not match the syntax for the second format), the value indicates either a geographical timezone or a special timezone from an implementation-defined timezone database.` と述べており、これを字句通りに解釈するなら、コロン形式にもPOSIX形式 (the second format) にも合致しない場合に、地理的タイムゾーン形式 (the third format) として扱われることになる。glibcよりも、少数派のmuslの処理が適切と言えそうだ。

#### glibc

```console:glibc
$ TZ=EST5EDT date -d @-769395601 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T18:59:59-0400[EWT]
$ TZ=EST5EDT date -d @-769395600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T19:00:00-0400[EPT]

$ TZ=EST+5EDT date -d @-769395601 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T18:59:59-0400[EDT]
$ TZ=EST+5EDT date -d @-769395600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T19:00:00-0400[EDT]
```

上記先頭2例の`TZ=EST5EDT`のケースでは、タイムゾーン名称が"EWT" または "EPT" となっている。これらはPOSIX形式では現れない。tzdataの定義データを参照していることがわかる。なお、"EWT", "EPT" は それぞれ ["Eastern War Time", "Eastern Peace Time"](https://web.archive.org/web/20000926032210/http://rowayton.org/rhs/hstaug45.html)の略である。後半2例の`TZ=EST+5EDT`と5の前にプラスを付与した場合は、このゾーンIDをもつゾーンファイルは存在しないので、POSIX形式として解釈される。

以下、FreeBSD, macOSの例も示すが、同様である。

#### FreeBSD

```console:FreeBSD
$ TZ=EST5EDT date -r -769395601 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T18:59:59-0400[EWT]
$ TZ=EST5EDT date -r -769395600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T19:00:00-0400[EPT]

$ TZ=EST+5EDT date -r -769395601 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T18:59:59-0400[EDT]
$ TZ=EST+5EDT date -r -769395600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T19:00:00-0400[EDT]
```

#### macOS

```console:macOS
$ TZ=EST5EDT date -r -769395601 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T18:59:59-0400[EWT]
$ TZ=EST5EDT date -r -769395600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T19:00:00-0400[EPT]

$ TZ=EST+5EDT date -r -769395601 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T18:59:59-0400[EDT]
$ TZ=EST+5EDT date -r -769395600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T19:00:00-0400[EDT]
```

#### musl

muslは、上記の通り、優先的にPOSIX形式として解釈しようとする。

```console:musl
$ TZ=EST5EDT date -d @-769395601 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T18:59:59-0400[EDT]
$ TZ=EST5EDT date -d @-769395600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T19:00:00-0400[EDT]

$ TZ=EST+5EDT date -d @-769395601 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T18:59:59-0400[EDT]
$ TZ=EST+5EDT date -d @-769395600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T19:00:00-0400[EDT]

# 確実にtzdata側を使用したいなら、コロン形式とする
$ TZ=:EST5EDT date -d @-769395601 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T18:59:59-0400[EWT]
$ TZ=:EST5EDT date -d @-769395600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1945-08-14T19:00:00-0400[EPT]
```

## 変更点2. POSIX形式でのDST遷移ルールが省略された場合の挙動が処理系定義とされた

POSIX形式は、`std offset[dst[offset][,rule]]` の形をとる。DST(いわゆる夏時間)が採用されるタイムゾーンでは、標準時←→DSTの切り替わりがいつ発生するかのルールを `rule` 部 (以下「ルール部」) で指定できる。シンタックス定義上ルール部は省略可能とされているが、POSIX-1.2017では ルール部を省略した場合の結果について未記述であった[^silent]。POSIX-1.2024では、ルール部を省略した場合のルールは処理系定義と明示された。

POSIX準拠の処理系においては、POSIXが処理系定義と規定したものについて、その挙動をドキュメントで明示しなければならない (未規定 (unspecified) のものについてはドキュメントは要求されない)。POSIX-1.2017以前ではDST切り替わりルールが省略された場合の挙動を明示する必要は無かったが、POSIX-1.2024に準拠する場合、それを明示する必要がある。実際にglibcではバージョン2.40から、POSIX-1.2024の処理系定義への規定変更に伴い、その挙動がglibcマニュアルで明示されるように[更新](https://sourceware.org/git/?p=glibc.git;a=commit;h=ee768a30fe9099972dad04c49972c2508a208b9b)された (なお、この更新を行ったのは、IANA tzdataメンテナーの [Paul Eggert](https://samueli.ucla.edu/time-zone-king-how-one-ucla-computer-scientist-keeps-digital-clocks-ticking/) である)。念のため補足しておくと、ルール部が省略された場合のglibc挙動については、実は Linux man-pages project の成果物の[tzset(3)](https://www.man7.org/linux/man-pages/man3/tzset.3.html)にごく簡単な言及はあったが、これはglibc公式のドキュメントではない。[FreeBSD libc](https://man.freebsd.org/cgi/man.cgi?query=tzset)や[macOS libc](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/tzset.3.html)では、以前からルール省略時の挙動について、ドキュメントで明示されている。[musl](https://wiki.musl-libc.org/environment-variables.html)は、本稿執筆現在においてドキュメントに記述は無い (間に合っていないというのが実態であろう)。

[^silent]: 特定条件での結果や挙動について、POSIXに記述/言及が存在しないものを、本稿では「未記述」と呼ぶ。「未記述」は本稿独自の用語である。POSIXそれ自体においては、言及がないことを "silent/silence" と表現しており (一般単語として用いられており用語としての定義は無い)、「未規定」(unspecified) に相当するとしている (Silence is intended to be equivalent to the term "unspecified". [参照](https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/xrat/V4_xbd_chap01.html#tag_21_01_16))。本稿では、POSIXの記述実態として明示的に「未規定」とされたものと何も言及が無いものとの違いを強調する目的で「未記述」と表現した箇所があるが、POSIX仕様内容の解釈として「未規定」と「未記述」は同様に扱われる。

### POSIX形式ルール省略時の処理系の挙動

まず始めに注意点を述べておく。本稿執筆時点において、残念ながらルール省略時の挙動に処理系間の移植性は無い。移植性を考慮したアプリケーションはルールを省略すべきではない。

#### FreeBSD / macOS

FreeBSD, macOSは、挙動が理解しやすい。

ゾーンファイル `posixrules` ファイル (通常 `/usr/share/zoneinfo/posixrules` にある) が存在する場合、現地時間ベースで`posixrules`の標準時←→DST切り替わりルールが使用される。通常 `posixrules` ファイルの内容は、米東部時間を定める `America/New_York` に等しくなる。これは、3月第2日曜日の現地時間午前2時に標準時からDSTに切り替わり、11月第1日曜日の現地時間午前2時にDSTから標準時に切り替わるルールである。

以下は、macOSの例だがFreeBSDでも同様の結果となる。

```console:macOS
# 米東部時間 標準時 → DST 切り替わり 
$ TZ=EST+5EDT date -r 1710053999 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-10T01:59:59-0500[EST]
$ TZ=EST+5EDT date -r 1710054000 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-10T03:00:00-0400[EDT]

# 米東部時間 DST → 標準時 切り替わり 
$ TZ=EST+5EDT date -r 1730613599 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-03T01:59:59-0400[EDT]
$ TZ=EST+5EDT date -r 1730613600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-03T01:00:00-0500[EST]

# 米中部時間 標準時 → DST 切り替わり 
$ TZ=CST+6CDT date -r 1710057599 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-10T01:59:59-0600[CST]
$ TZ=CST+6CDT date -r 1710057600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-10T03:00:00-0500[CDT]

# 米中部時間 DST → 標準時 切り替わり 
$ TZ=CST+6CDT date -r 1730617199 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-03T01:59:59-0500[CDT]
$ TZ=CST+6CDT date -r 1730617200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-03T01:00:00-0600[CST]

$ sha256sum /usr/share/zoneinfo/America/New_York /usr/share/zoneinfo/posixrules
e9ed07d7bee0c76a9d442d091ef1f01668fee7c4f26014c0a868b19fe6c18a95  /usr/share/zoneinfo/America/New_York
e9ed07d7bee0c76a9d442d091ef1f01668fee7c4f26014c0a868b19fe6c18a95  /usr/share/zoneinfo/posixrules
```

`posixrules` が存在しない場合、3月第2日曜日の現地時間午前2時に標準時からDSTに切り替わり、11月第1日曜日の現地時間午前2時にDSTから標準時に切り替わるルールとなる。これは米国本土のDSTを採用するタイムゾーン定義に等しく、米国の時間制度をデフォルトとなる。

上記のルールをPOSIX形式として省略せずに記述した場合は、ルール部は `M3.2.0/2,M11.1.0/2` に等しくなる。米東部時間は、`TZ=EST+5EDT,M3.2.0/2,M11.1.0/2` であり、米中部時間は`TZ=CST+6CDT,M3.2.0/2,M11.1.0/2`。実際の米国の時間制度とも合致しており、分かりやすい。米タイムゾーンを用いる限りにおいて、省略時のルールはアプリケーションの意図通りに動くであろう。

#### glibc

glibcでは、ルール部を省略すべきではない。本稿執筆現在の最新バージョン2.40でも[不具合](https://sourceware.org/bugzilla/show_bug.cgi?id=32118)があり、さらには2.36以前と2.37以降(2.40含む)とでバージョン間の挙動も異なる。

glibcも、上記のmacOSやFreeBSDと同様に、`posixrules` ファイルがあればそれを参照する。またglibcが用いられるほとんどのLinuxディストリビューションで提供されるtzdataパッケージもmacOSと同様に、`posixrules` のタイムゾーンルールは、米東部時間の `America/New_York` と同一に設定されている。しかしながら、glibcは、その取り扱い方が、FreeBSD等とは異なる挙動をし、一貫性も無い。

まずは、バージョン2.36以前を見てみる。

:::note info
glibc 2.36および2.37の動作検証は、glibcアップストリームとしての挙動を示すために、Linuxディストリビューション提供のglibcではなく、GNUプロジェクトが配布するソースをローカルビルドしたものを用いた。以下のコマンドサンプルにある `./testrun.sh` は、OSのglibcではなくローカルビルドのglibcを用いて実行できるようにするために [GNUプロジェクト公式が提供するラッパースクリプト](https://sourceware.org/glibc/wiki/Testing/Builds#Compile_normally.2C_run_under_new_glibc)である。
:::

```console:glibc 2.36
$ ./testrun.sh ./libc.so.6  | grep 'libc.*version'
GNU C Library (GNU libc) stable release version 2.36.

# 米東部時間(UTC-05:00)の場合は、posixrulesと完全に合致する
$ TZ=EST+5EDT ./testrun.sh /usr/bin/date -d @1710053999 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-10T01:59:59-0500[EST]
$ TZ=EST+5EDT ./testrun.sh /usr/bin/date -d @1710054000 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-10T03:00:00-0400[EDT]

# DST→標準時も同様である。
$ TZ=EST+5EDT ./testrun.sh /usr/bin/date -d @1730613599 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-03T01:59:59-0400[EDT]
$ TZ=EST+5EDT ./testrun.sh /usr/bin/date -d @1730613600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-03T01:00:00-0500[EST]

# 米中部時間(UTC-06:00)では、現地時間ベースで米東部時間よりもマイナス2時間の現地時間午前0時に標準時→DSTへ切り替わる
# これは現地時間ベースでもUTCベースでも posixrulesと合致していない。
$ TZ=CST+6CDT ./testrun.sh /usr/bin/date -d @1710050399 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-09T23:59:59-0600[CST]
$ TZ=CST+6CDT ./testrun.sh /usr/bin/date -d @1710050400 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-10T01:00:00-0500[CDT]

# DST→標準時の切り替わりも同様に現地時間午前0時となってしまう。
$ TZ=CST+6CDT ./testrun.sh /usr/bin/date -d @1730609999 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T23:59:59-0500[CDT]
$ TZ=CST+6CDT ./testrun.sh /usr/bin/date -d @1730610000 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T23:00:00-0600[CST]

# 米山岳部時間 (UTC-07:00) では、現地時間ベースで米中部時間よりさらに2時間マイナスされた、前日の午後22:00に標準時→DSTへ切り替わる
$ TZ=MST+7MDT ./testrun.sh /usr/bin/date -d @1710046799 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-09T21:59:59-0700[MST]
$ TZ=MST+7MDT ./testrun.sh /usr/bin/date -d @1710046800 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-09T23:00:00-0600[MDT]

# DST→標準時の切り替わりも同様である。
$ TZ=MST+7MDT ./testrun.sh /usr/bin/date -d @1730606399 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T21:59:59-0600[MDT]
$ TZ=MST+7MDT ./testrun.sh /usr/bin/date -d @1730606400 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T21:00:00-0700[MST]
```

上記の通りバージョン2.36以前は米東部時間 (EST+5EDT) のみ期待通り動作するが、それ以外のタイムゾーンは米東部時間との時差x2だけ現地時間ベースでずれ込む。もちろん現実の時間制度とも合致しない。おもに米ユーザー限定ではあるが、現実の時間制度に合致するFreeBSD, macOS相当の挙動を期待するユーザーは面食らうだろう。もっとも、解釈の一つとして、`posixrules` はその定義が米東部時間のものなので`EST+5EDT`のみと挙動が合致しているのだ、と説明されれば納得するしかない。

続いてバージョン2.37の挙動である。これは2.36よりもひどく、`EST+5EDT` の場合すら`posixrules`と合致していない。ルール省略時に`posixrules` を参照することを定めた[2.40の文書規定](https://sourceware.org/glibc/manual/2.40/html_node/Proleptic-TZ.html)から明らかに逸脱しており、[不具合](https://sourceware.org/bugzilla/show_bug.cgi?id=32118)と言える (2.40より前は文書規定が無かったので明確に不具合とは言えなかった)。

```console:glibc 2.37
$ ./testrun.sh ./libc.so.6  | grep 'libc.*version'
GNU C Library (GNU libc) stable release version 2.37.

# 米東部時間の標準時→DSTの切り替わりは変化ない
$ TZ=EST+5EDT ./testrun.sh /usr/bin/date -d @1710053999 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-10T01:59:59-0500[EST]
$ TZ=EST+5EDT ./testrun.sh /usr/bin/date -d @1710054000 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-10T03:00:00-0400[EDT]

# 米東部時間のDST→標準時は、驚くべきことに現地時間前日の22:00に切り替わってしまう。
# これは2.36以前と互換性がなくposixrulesとも整合しない
$ TZ=EST+5EDT ./testrun.sh /usr/bin/date -d @1730599199 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T21:59:59-0400[EDT]
$ TZ=EST+5EDT ./testrun.sh /usr/bin/date -d @1730599200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T21:00:00-0500[EST]

# 米中部時間も標準時→DST切り替わりは2.36と同じである (2.36自体が予想に反するのは前述の通りであるが...)
$ TZ=EST+6EDT ./testrun.sh /usr/bin/date -d @1710050399 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-09T23:59:59-0600[EST]
$ TZ=EST+6EDT ./testrun.sh /usr/bin/date -d @1710050400 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-10T01:00:00-0500[EDT]

# 米中部時間DST→標準時は、現地時間前日の20:00に切り替わる。
$ TZ=EST+6EDT ./testrun.sh /usr/bin/date -d @1730595599 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T19:59:59-0500[EDT]
$ TZ=EST+6EDT ./testrun.sh /usr/bin/date -d @1730595600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T19:00:00-0600[EST]

# 米山岳時間も標準時→DST切り替わりは2.36と同じである
$ TZ=MST+7MDT ./testrun.sh /usr/bin/date -d @1710046799 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-09T21:59:59-0700[MST]
$ TZ=MST+7MDT ./testrun.sh /usr/bin/date -d @1710046800 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-09T23:00:00-0600[MDT]

# 米山岳時間のDST→標準時は、現地時間前日の18:00に切り替わる。
$ TZ=CST+7CDT ./testrun.sh /usr/bin/date -d @1730591999 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T17:59:59-0600[CDT]
$ TZ=CST+7CDT ./testrun.sh /usr/bin/date -d @1730592000 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T17:00:00-0700[CST]
```

実際に `posixrules` あるいはその他の地理的タイムゾーン形式と比べる形で、あらためてglibc 2.37以降の問題を端的に示す:

```console
# 以下は期待通り
$ TZ=America/New_York ./testrun.sh /usr/bin/date -d @1730599200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T22:00:00-0400[EDT]
$ TZ=posixrules ./testrun.sh /usr/bin/date -d @1730599200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T22:00:00-0400[EDT]
$ TZ=EST5EDT ./testrun.sh /usr/bin/date -d @1730599200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T22:00:00-0400[EDT]

# 以下は明らかに不具合と言える
$ TZ=EST+5EDT ./testrun.sh /usr/bin/date -d @1730599200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-11-02T21:00:00-0500[EST]
```

```console
$ sha256sum /usr/share/zoneinfo/America/New_York /usr/share/zoneinfo/posixrules
e9ed07d7bee0c76a9d442d091ef1f01668fee7c4f26014c0a868b19fe6c18a95  /usr/share/zoneinfo/America/New_York
e9ed07d7bee0c76a9d442d091ef1f01668fee7c4f26014c0a868b19fe6c18a95  /usr/share/zoneinfo/posixrules
```

### musl

muslは、glibc以上にルールを省略してはならない。ルールを省略した場合、夏時間UTCオフセットが0時間以上のタイムゾーンでは1年中夏時間となり、夏時間UTCオフセットが0時間未満のタイムゾーンでは、年の最後の1時間 (12月31日23:00:00から23:59:59) のみが標準時、それ以外は夏時間となってしまう。

```console:musl
# 標準時ベースで 12月31日 23:00:00 - 23:59:59の間の1時間だけ標準時となる
$ TZ=EST+5EDT date -d @1704081599 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2023-12-31T23:59:59-0400[EDT]
$ TZ=EST+5EDT date -d @1704081600 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2023-12-31T23:00:00-0500[EST]
$ TZ=EST+5EDT date -d @1704085199 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2023-12-31T23:59:59-0500[EST]
$ TZ=EST+5EDT date -d @1704085200 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-01-01T01:00:00-0400[EDT]
```

しかしながらこれは、glibcとは異なり、muslの不具合というよりも、単純にmuslはルールの省略をサポートしていないと考えるべきである。ルール省略時のハンドリング処理を無くすことで効率性を高められると言える。サポートしないこと自体はPOSIX定義上も全く問題ない。ただしPOSIX-1.2024による処理系定義規定に合わせて、muslも将来的にその挙動をドキュメントで明示することが期待される。

## 変更点3 POSIXタイムゾーン形式のdst ruleに指定可能な時刻の範囲が変わった

POSIX.1-2017以前は、POSIX形式における標準時↔DSTの切り替わり時刻のHourは0～24の範囲で指定可能とされていたが、POSIX.1-2024では、-167～167の範囲に拡張された。この拡張により、例えば翌日の23:30を意味する「47:30」や、前日の20:30を意味する「-03:30」などの指定が可能となる。これはglibc等の独自拡張として以前から利用できたが、POSIX.1-2024にて標準仕様となった。

この変更が有用になる具体的なユースケースとしてイスラエルの夏時間遷移ルール指定がある。本稿執筆現在、イスラエルの夏時間開始日時は、3月最終日曜日の直前の金曜日午前2:00 (現地時間) である。以前のPOSIX形式仕様では、このルールを指定できなかった。POSIX.1-2024では、`TZ=IST-2IDT,M3.5.0/-46,M10.5.0/2` のように、最終日曜日0時から46時間マイナスした時刻を指定する方法が標準として認められる。

なお、`TZ='XST5XDT,0/0,J365/25'` のように書けば年中DSTであるタイムゾーンとなる。

### 処理系の挙動

### glibc, FreeBSD, musl

glibc, FreeBSD libc, muslは、既にPOSIX-1.2024の新定義をサポートしている。

```console:glibc
$ TZ='IST-2IDT,M3.5.0/-46,M10.5.0/2' date -d @1711670399 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-29T01:59:59+0200[IST]
$ TZ='IST-2IDT,M3.5.0/-46,M10.5.0/2' date -d @1711670400 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-29T03:00:00+0300[IDT]
```

```console:FreeBSD
$ TZ='IST-2IDT,M3.5.0/-46,M10.5.0/2' date -r 1711670399 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-29T01:59:59+0200[IST]
$ TZ='IST-2IDT,M3.5.0/-46,M10.5.0/2' date -r 1711670400 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-29T03:00:00+0300[IDT]
```

```console:musl
$ TZ='IST-2IDT,M3.5.0/-46,M10.5.0/2' date -d @1711670399 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-29T01:59:59+0200[IST]
$ TZ='IST-2IDT,M3.5.0/-46,M10.5.0/2' date -d @1711670400 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-29T03:00:00+0300[IDT]
```

### macOS

macOS libcは、本稿執筆現在、POSIX-1.2024の新定義をサポートしていない。0-24の範囲外のHourを指定した場合、不正値とみなされUTCにフォールバックする。

```console:macOS
$ TZ='IST-2IDT,M3.5.0/-46,M10.5.0/2' date -r 1711670399 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-28T23:59:59+0000[UTC]
$ TZ='IST-2IDT,M3.5.0/-46,M10.5.0/2' date -r 1711670400 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
2024-03-29T00:00:00+0000[UTC]
```

## その他処理系の挙動

### コロン形式のハンドリングの違い

TZ値をコロンで始めることでその解釈を処理定義とするコロン形式が規定あることを[先述](#補足-posix-12017以前のtz規定)した。

#### glibc

glibcはコロンを無視する。すなわちコロンを除いた結果、地理的タイムゾーン形式に合致すればそれとして、POSIXタイムゾーン形式に合致すればそれとして扱われる。

```console:glibc
# 地理的タイムゾーン形式では、コロン有無で同じ結果となる
$ TZ=:Asia/Tokyo date -d @0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]
$ TZ=Asia/Tokyo date -d @0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]

# POSIX形式でも、コロン有無で同じである。
$ TZ=:JST-9 date -d @0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]
$ TZ=JST-9 date -d @0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]
```

#### FreeBSD, macOS, musl

FreeBSD, macOS, muslは、コロン形式は地理的タイムゾーン形式とみなされる。地理的タイムゾーン形式として不正な場合 (TZに指定されたゾーンIDに紐づくゾーンファイルが見つからない場合)、UTCにフォールバックする。

```console:FreeBSD
$ TZ=:Asia/Tokyo date -r 0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]
$ TZ=Asia/Tokyo date -r 0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]

# コロンに続いてPOSIX形式を指定した場合UTCにフォールバックする
$ TZ=:JST-9 date -r 0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T00:00:00+0000[UTC]
$ TZ=JST-9 date -r 0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]
```

```console:macOS
$ TZ=:Asia/Tokyo date -r 0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]
$ TZ=Asia/Tokyo date -r 0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]

# コロンに続いてPOSIX形式を指定した場合UTCにフォールバックする
$ TZ=:JST-9 date -r 0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T00:00:00+0000[UTC]
$ TZ=JST-9 date -r 0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]
```

```console:musl
$ TZ=:Asia/Tokyo date -d @0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]
$ TZ=Asia/Tokyo date -d @0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]

# コロンに続いてPOSIX形式を指定した場合UTCにフォールバックする
$ TZ=:JST-9 date -d @0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T00:00:00+0000[UTC]
$ TZ=JST-9 date -d @0 '+%Y-%m-%dT%H:%M:%S%z[%Z]'
1970-01-01T09:00:00+0900[JST]
```

## まとめ

| 観点                               | POSIX-1.2017 | POSIX-1.2024 | glibc            |FreeBSD    | macOS  | musl         | 
|-----------------------------------|---------------|--------------|------------------|------------|--------|-------------|
| 地理的形式規定                     | 未規定         | 処理系定義    | 対応             | 対応         | 対応         | 対応     |
| 優先順位<br>地理的形式 vs POSIX形式 | 未規定         | POSIX形式    | 地理的形式       | 地理的形式     | 地理的形式      | POSIX形式  |
| POSIX形式<br>ルール省略時挙動          | 未規定      | 処理系定義   | 事実上非対応      | 米統一時間| 米統一時間      | 非対応  |
| コロン形式                         | 処理系定義      | 処理系定義   | コロンを無視     | tzdata        | tzdata     | tzdata |
| DST変換ルール時刻範囲               | 0～24          | -167~167    | -167~167        | -167~167 | 0～24 |-167~167      | 

## 参考資料

- [POSIX-1.2017, Base Definitions 8.3 Other Environment Variables](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_03)
- [POSIX-1.2024, Base Definitions 8.3 Other Environment Variables](https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap08.html#tag_08_03)
- [The GNU C Library (glibc) manual version 2.39, 22.5.6 Specifying the Time Zone with TZ](https://sourceware.org/glibc/manual/2.39/html_node/TZ-Variable.html)
- [The GNU C Library (glibc) manual version 2.40, 22.5.6.2 Proleptic Format for TZ](https://sourceware.org/glibc/manual/2.40/html_node/Proleptic-TZ.html)
- [FreeBSD Manual Pages tzset(3)](https://man.freebsd.org/cgi/man.cgi?query=tzset)
- [Mac OS X Man Pages tzset(3)](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/tzset.3.html)
- [musl manual Environment Variables](https://wiki.musl-libc.org/environment-variables.html#%3Ccode%3ETZ%3C/code%3E)