2019/05/05(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で書くから!