The Man Who Fell From The Wrong Side Of Sky:2019年5月5日分

2019/5/5(Sun)

[Windows] PowerShellで生活するために - Get-Dateコマンドレット編(その2)

試験運用中のWindows 10 1809に KB4495667が降ってきて令和表示されるようになったな、5/3リリースは早いのか遅いのか。

そいや銀行系で 令和元年が平成元年に化けて振込予定日が1989年にタイムスリップというバグ出したようだけどどんなクソコード書いてるのか空恐ろしいですわね、ワイなら預金全額以下略(風説の呂布)。

@謎解き

そんでは 昨日の続き。

  • DateTime型へのキャスト
  • Get-Dateの-Dateオプションによる文字列からのDateTimeオブジェクトへの変換

これはどちらも内部的には.Net Frameworkの DateTimeクラスのParse(String)メソッドを呼び出しているのだ、PowerShellからだと

[DateTime]::Parse("2019/05/01")

と書く。

そんでマニュアル(クソみてえな精度の自動翻訳に脳血管が切れないようen-usを推奨)を読むとParse(String)は

  • Parse(String, IFormatProvider)
  • Parse(String, IFormatProvider, DateTimeStyles)

と2つのオーバーライドが存在するのだ。

そしてこいつらの2つめの引数である IFormatProviderインタフェースに注目しよう、こいつはある特定の言語/地域の日付形式をパースしたり表示したい場合に指定するものなんですわ。

そんでPowerShellにおけるDateTime型へのキャストにおいては

  • 現在のOSの言語/地域設定は無視される
  • 必ずen-USとして日時を表す文字列はパースされる

ということで

[DateTime]"2019/05/01"

[DateTime]::Parse("2019/05/01", [System.Globalization.CultureInfo]::InvariantCulture)
[DateTime]::Parse("2019/05/01", [System.Globalization.CultureInfo]::CreateSpecificCulture("en-US"))

と等価なんですな、この InvariantCultureについてMicrosoftは

Gets the CultureInfo object that is culture-independent (invariant).

と「文化に依存しない(普遍の)」とかふざけたこと抜かしてるわけだけどen-USとかどこの田舎者だクソが。

一方でGet-Dateを使った場合は、現在のスレッドにおける言語/地域の設定を尊重するので

Get-Date "2019/05/01"

[DateTime]::Parse("2019/05/01")
[DateTime]::Parse("2019/05/01", $null)
[DateTime]::Parse("2019/05/01", [System.Threading.Thread]::CurrentThread.CurrentCulture)

あるいは現在の言語地域設定が日本語/日本であるならば

[DateTime]::Parse("2019/05/01", [System.Globalization.CultureInfo]::CreateSpecificCulture("ja-JP"))

と等価になるわけ。

その結果Get-Dateを使ったコードは、システムのカレンダーを和暦に変更すると2019年を西暦ではなく令和2019年と解釈してしまうのだ。

つーかこれさぁja-JPの場合であったとしても

  • 「2019/05/01」のように元号指定がなければ西暦として扱う
  • 「R1/05/01」のように先頭に元号指定があれば和暦として扱う

方が混乱無くていいんじゃないですかね、もはやこれバグの類だと思うゾ (日本人に限らずカレンダーの種類複数ある文化圏ならどっちか選ぶというより併用だよね)。

@結論

個人的な見解としては

  • DateTime型へのキャストがen-US固定なのはバグ、ちゃんと現在のスレッドの言語設定を尊重しろ
  • しかしDateTime.Parse("ja-JP") + 和暦カレンダーの場合、先頭に元号(MTSHR)を表す文字が無くても西暦ではなく和暦として変換するのもバグ

なんだけどまあ「仕様です」で終わりだろうな、ということで前回「UNIXシェルスクリプトで日付計算するやつはバカだ」と書いたけど、PowerShellで日付計算するやつもバカなのだ。

対策としていっちゃん簡単なのは国際化アプリケーションとしては失格だけど、そもそもその国際化がバグってて日付が2019年先に化ける危険性があるなら

  • 入力は西暦のみで和暦は禁止
  • Parse()ではなくParseExact()を使ってカスタム書式を使って厳密化

とでもしたほうがよっぽどマシってとこなんだけど、これも仕様をちゃんと読まずにコード書いて

Write-Host ([DateTime]::ParseExact("2019/05/01","yyyy/MM/dd", $null)).ToLongDateString()

とか書いちゃうと

令和2019年5月1日

に化けるのよね、勘違いしやすいところだけどWindowsそして.Net Frameworkの カスタム日時書式指定文字列において「yyyy」は西暦の意味ではなくカレンダーが和暦ならそれは和暦になるのよな。 そして第3引数のIFormatProviderにNULL指定してるのでシステムのカレンダーが使われ、そいつが和暦に設定してあればそりゃ令和2019年に化けるよという。

これがUNIXだったら「%Y」が西暦で「%EY」が和暦だし、そもそも同じMicrosoft製品でもOfficeだと「yyyy」が西暦で「gee」が和暦と明確に分けられてるのにね、そっちに慣れてると混同してしまい易いのよね。

なので必ずParseでもParseExactでも第2引数の言語/地域は指定しておけ。

$loc = [System.Globalization.CultureInfo]::InvariantCulture
Write-Host ([DateTime]::Parse("2019/05/01", $loc)).ToLongDateString()
Write-Host ([DateTime]::ParseExact("2019/05/01","yyyy/MM/dd", $loc)).ToLongDateString()

はーつっかえ、じゃ俺はPerlで書くから!