Home > Windows にまつわる e.t.c.

サマータイムを考慮した PowerShell での時刻ハンドリング


何やら日本でもサマータイムが導入されるかもって騒ぎになっているので、PowerSehll でサマータイムをどうハンドリングすれば良いのか調べたメモです。

結論を先に言うと、.NET Framework/.NET Core がサマータイムを解決してくれるので、下手に自力実装せずに .NET を使うと幸せになれそうです。

 

サマータイムで起きること

サマータイムは現在時刻を進めて夏時間にします。このためサマータイムが開始されるときには、切り替わり時間が時刻として存在しなくなります。

サマータイムを導入している太平洋標準時間のシアトル時間を例にするとこんな感じになります。

サマータイム開始 : 2018/3/11 2:00

シアトル時間 2018/3/11 0:00 2018/3/11 1:00 |2018/3/11 3:00 2018/3/11 4:00

2018/3/11 2:00 が存在しない

 

逆に、サマータイムが終了する時は現在時刻を遅らせます。このため同じ時刻が2回発生することになります。

サマータイム終了 2018/11/4 2:00

シアトル時間 2018/11/4 0:00 2018/11/4 1:00 2018/11/4 1:00 2018/11/4 2:00 2018/11/4 3:00

2018/11/4 1:00 が2回発生する

 

DateTime と DateTimeOffset の違い

PowerSehll で通常時刻をハンドリングする際は、DateTime を使いますよね。

Get-Date 等の標準的な時刻は DateTime です。

$DateTime = Get-Date
$DateTime.GetType()
$DateTime | fl *

 

このように、DateTime はローカル時間しか持っていないので、先にも説明したようにサマータイム運用が始まると、特定の時間が欠落したり二重に発生したりと何かと不都合です。

一方、DateTimeOffset はローカル時間と UTC の両方をハンドリングしています。

$DateTimeOffset = [System.DateTimeOffset]::Now
$DateTimeOffset.GetType()
$DateTimeOffset | fl *

 

このように、DateTimeOffset では DateTime にはローカル時間が、UtcDateTime に UTC がセットされているので、システムが使用する時刻として都合が良い時刻を使用することが出来ます。

DateTimeOffset を使えば全てが解決というわけではなく、既に作成しているスクリプトやシステムの手直しが必要ですが、ローカル時間と UTC を別にハンドリングする必要が無いので無用な bug を招くリスクは軽減されます。

 

現在のローカル時刻と UTC の取得

DateTimeOffset には、現在のローカル時刻とUTCを取得するメソッドがありますので、Get-Date をキャストして DateTimeOffset にする必要はありません。

# ローカル時刻
$LocalNow = [System.DateTimeOffset]::Now
$LocalNow.DateTime

# UTC
$UtcNow = [System.DateTimeOffset]::UtcNow
$UtcNow.DateTime

 

任意のタイムゾーンの時刻をセットする

DateTimeOffset は、タイムゾーンに基づいた動作をしており、コンピューターにセットされているタイムゾーンと UTC を扱う仕様です。

このため、現在セットされているタイムゾーンとは異なった任意のタイムゾーンの時刻をセットするのにはひと工夫が必要です。

例えば、本社が日本にあるので他国で展開している PC でも JST を扱う必要がある場合とかのケースですね。

 

タイムゾーンの ID を確認する

タイムゾーンを指定するには、タイムゾーンの ID を指定します。

JST の ID は「Tokyo Standard Time」です。シアトルで使われている PST の ID は「Pacific Standard Time」です。

ID にどのようなものがあるかは GetSystemTimeZones で確認できます。

$SystemTimeZones = [System.TimeZoneInfo]::GetSystemTimeZones()
$SystemTimeZones.Count
$SystemTimeZones[0]

 

結構な数のタイムゾーンがハンドリングできることがわかります。

 

DateTime を 任意のタイムゾーン時刻の DateTimeOffset に変換する

それでは、2018/9/5 10:00 をシアトル時間である PST にセットしてみましょう。

手順としては、DateTime に任意の時刻をセットして、そいつを指定タイムゾーン時刻の DateTimeOffset に変換する感じです。

# セットする日時
$DateTime = [datetime]"2018/9/5 10:00"

# 指定タイムゾーンの TimeSpan を求める
$TimeSpan = ([System.TimeZoneInfo]::FindSystemTimeZoneById("Pacific Standard Time")).GetUtcOffset($DateTime)

# TimeSpan を指定して DateTimeOffset を作る
$PST = New-Object System.DateTimeOffset( $DateTime, $TimeSpan )

$PST

 

ミソは FindSystemTimeZoneById で ID を指定して TimeSpan を求めている所です。

これで、任意のタイムゾーンの DateTimeOffset をハンドリングする事が出来ます。

 

サマータイムで発生するローカル時間の揺れをハンドリングする

単純に時刻変換するだけで済まないのがサマータイムの厄介な所です。

冒頭に説明したように、サマータイム開始時には特定のローカル時刻が消えてしまいますし、サマータイムが終了する時には特定の時刻が二重に発生しますので、こいつを判断する必要があります。

 

存在しなくなるローカル時刻の確認

セットした時刻が、サマータイム開始時に存在しなくなる時刻に該当しているかを確認するには、TimeZoneInfo の IsInvalidTime を使います。

# セットする日時
$DateTime = [datetime]"2018/9/5 10:00"

# 指定タイムゾーンの TimeSpan を求める
$TimeSpan = ([System.TimeZoneInfo]::FindSystemTimeZoneById("Pacific Standard Time")).GetUtcOffset($DateTime)

# TimeSpan を指定して DateTimeOffset を作る
$PST = New-Object System.DateTimeOffset( $DateTime, $TimeSpan )

# ID でタイムゾーン情報を取得する
$PstTimeZoneInfo = [System.TimeZoneInfo]::FindSystemTimeZoneById("Pacific Standard Time")

# 存在する時刻か否か
$IsInvalidTime = $PstTimeZoneInfo.IsInvalidTime( $PST.DateTime )

$IsInvalidTime

 

IsInvalidTime で得られた bool 値が False の場合は存在する時刻ですが、True になった場合は存在しない時刻になってしまったのでエラートラップする必要があります。(2018/3/11 2:00 をセットすると、True になります)

 

二重に発生する時刻の確認

変換した時刻がサマータイム終了時に発生する 二重発生時刻かの確認をする場合は、IsAmbiguousTime を使います。

先ほどと同様に、時刻をセットし、IsAmbiguousTime で確認をします。

# セットする日時
$DateTime = [datetime]"2018/9/5 10:00"

# 指定タイムゾーンの TimeSpan を求める
$TimeSpan = ([System.TimeZoneInfo]::FindSystemTimeZoneById("Pacific Standard Time")).GetUtcOffset($DateTime)

# TimeSpan を指定して DateTimeOffset を作る
$PST = New-Object System.DateTimeOffset( $DateTime, $TimeSpan )

# ID でタイムゾーン情報を取得する
$PstTimeZoneInfo = [System.TimeZoneInfo]::FindSystemTimeZoneById("Pacific Standard Time")

### ここまでは一緒

# 二重発生時刻か否か
$IsAmbiguousTime = $PstTimeZoneInfo.IsAmbiguousTime( $PST.DateTime )

$IsAmbiguousTime

 

IsAmbiguousTime で得られた bool 値が True になった場合は二重存在する時刻なので対処が必要です(2018/11/4 1:00 をセットすると、True になります)

 

二重に発生する時刻のオフセットを求める

この時に、二重発生する時刻のオフセットは GetAmbiguousTimeOffsets で得る事が出来ます

# セットする日時
$DateTime = [datetime]"2018/11/4 1:00"

# 指定タイムゾーンの TimeSpan を求める
$TimeSpan = ([System.TimeZoneInfo]::FindSystemTimeZoneById("Pacific Standard Time")).GetUtcOffset($DateTime)

# TimeSpan を指定して DateTimeOffset を作る
$PST = New-Object System.DateTimeOffset( $DateTime, $TimeSpan )

# ID でタイムゾーン情報を取得する
$PstTimeZoneInfo = [System.TimeZoneInfo]::FindSystemTimeZoneById("Pacific Standard Time")

# 二重発生時刻か否か
$IsAmbiguousTime = $PstTimeZoneInfo.IsAmbiguousTime( $PST.DateTime )

$IsAmbiguousTime

### ここまでは一緒

$GetAmbiguousTimeOffsets = $PstTimeZoneInfo.GetAmbiguousTimeOffsets( $PST.DateTime )
$GetAmbiguousTimeOffsets

 

このように 二重発生のオフセットをそれぞれ得ることが出来るので、この場合も対応する処理を書く必要があります。

 

現地時刻を得る

TimeZoneInfo は異なるタイムゾーンのローカル時刻を TimeZoneInfo の ConvertTimeBySystemTimeZoneId で簡単に得ることが出来ます。

例えば、JST から PST に変換する場合は以下のようにします。

# 任意の時刻を DateTime にする
$DateTime = [datetime]"2018/10/13 14:00"

# JST の TimeSpan を求める
$TimeSpan = ([System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")).GetUtcOffset($DateTime)

# DateTime を JST な DateTimeOffset に変換する
$JST = New-Object System.DateTimeOffset( $DateTime, $TimeSpan )

# JST を PST に変換する
$PST = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId($JST, "Pacific Standard Time")

$PST

 

JST でサマータイムが実施された場合は、JST な DateTimeOffset に変換した際に先に説明したローカル時間の揺れをハンドリングする必要がありますが、タイムゾーンを超えた時刻変換が簡単に出来ます。

 

コンピューターの現在時刻を合わせる

コンピューターの現在時刻を合わせる場合は、外部から時刻をもらってくる必要があります。

本来これは NTP の仕事ですね。

ドメインメンバーであればドメインコントローラーから時刻もらうので、時刻を設定する必要はありません。

ワークグループの場合(ドメイン参加前も)は、Windows の NTP Client Service である W32Time が稼働していなかったり、W32Time の設定が正しくなく時刻が狂っていることも良くあります。

そんな時は、以下のように NICT が提供している Web API を使って現在時刻を合わせることができます。

# NICT が提供している Web API
$NictUri = "https://ntp-a1.nict.go.jp/cgi-bin/json"

# Unix Time の開始日時
$UnixTimeStart = [datetime]"1970/01/01"

# UTC の Time Zone ID
$UTCZoneID = "UTC"

# Unix Time の TimeSpan
$UnixTimeTimeSpan = ([System.TimeZoneInfo]::FindSystemTimeZoneById($UTCZoneID)).GetUtcOffset($UnixTimeStart)

# Unix Time の DateTimeOffset
$UnixTimeDateTimeOffset = New-Object System.DateTimeOffset( $UnixTimeStart, $UnixTimeTimeSpan )

# 現在の UTC
$UtcNow = $UnixTimeDateTimeOffset.AddSeconds((Invoke-RestMethod -Uri $NictUri).st)

# UTC をローカル時刻にする
$LocalNow = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId($UtcNow, [System.TimeZoneInfo]::Local.Id)

# 現在時刻の設定
Set-Date $LocalNow.DateTime

 

 

参考情報

DateTimeOffset 構造体 (System)
https://docs.microsoft.com/ja-jp/dotnet/api/system.datetimeoffset?WT.mc_id=WD-MVP-36880

DateTime、DateTimeOffset、TimeSpan、および TimeZoneInfo の使い分け | Microsoft Docs
https://docs.microsoft.com/ja-jp/dotnet/standard/datetime/choosing-between-datetime?WT.mc_id=WD-MVP-36880

ドメインの時刻をNICTのNTPと同期させる
http://www.vwnet.jp/Windows/WS08R2/NTP/w32time.html

 

 

back.gif (1980 バイト)

home.gif (1907 バイト)

Copyright © MURA All rights reserved.