- 現在時刻をUnixTimeへ変換
var baseDt = new DateTimeOffset(1970, 1, 1, 0, 0, TimeSpan.Zero);
var unixtime = (DateTimeOffset.Now - baseDt).Ticks/10000000;
// DateTimeOffset.NowはDateTimeOffset.UtcNowでも良い
- UnixTimeから変換
long unixtime = 1519372282; // 変換元unixtime
var baseDt = new DateTimeOffset(1970, 1, 1, 0, 0, TimeSpan.Zero);
var dt = new DateTimeOffset(unixtime*10000000 + baseDt.Ticks, TimeSpan.Zero);
解説
UnixTimeは1970年1月1日0時0分0秒からの秒数なので、C#からUnixTimeへの変換は1970/1/1 00:00:00を引いてから秒に変換すれば良い。C#の時間は単位が100ナノ秒なので、10000000(=10*1000*10000)で割る。UnixTimeからの変換は逆の操作になる。
DateTimeとDateTimeOffset
解説はこれですべてなのですが、C#で日時を扱うとき何クラスを使うべきか、という問題があります。結論は常にDateTimeOffsetを使っておけ、です。理由はDateTime
は機能が不足していて使えないからです。
DateTimeの特徴
- 基本的に日時しか扱えず、時差やタイムゾーンが扱えない。
- いちおう
Kind
プロパティで、Kind.Utc
(UTC)、Kind.Local
(現地時刻)、Kind.Unspecified
(指定なし)の3種類が指定できる。 - 逆に言うと、UTCと現地時刻(≒アプリケーションが動くPCのタイムゾーン)以外が扱えない。
-
Kind
プロパティの値が異なっていても、普通に足し算引き算ができてしまう。(もちろん結果は間違い) -
Kind
プロパティにKind.Unspecified
(指定なし)とか指定されても、どう扱えばよいか分からない。(多分タイムゾーンに関係ない、純粋な日時を値として使いたいときのプロパティ値か?) - 夏時間? そんなものはない。
という感じなので、いちいちKind
プロパティの値を気にして使用しなけらばならないし、Kind.Unspecified
のときどう扱ってよいか分からないし、特に最大の問題はKind
プロパティの認知度が低くて、バグを誘発しやすい、ということです。
DateTimeOffsetの使い方
そういう訳で、DateTimeOffsetを使おう、ということです。
- DateTimeOffsetは、内部で日時と標準時間との時差(
TimeSpan
)を保持している。 - 現在時刻は、
DateTimeOffset.Now
(タイムゾーンが現地時間)とDateTimeOffset.UtcNow
(標準時間)の2種類の方法で取得できる。
var dt1 = DateTimeOffset.Now;
var dt2 = DateTimeOffset.UtcNow;
Console.WriteLine($"{dt1}, {dt2}");
// → 2018/04/02 11:35:53 +09:00, 2018/04/02 2:35:53 +00:00
- コンストラクタで標準時間との時差を指定できる。現地時間を指定したい場合は、
TimeSpan
にTimeZoneInfo.Local.BaseUtcOffset
を指定する。夏時間もコレを指定しておけば、勝手に考慮してくれる。
var dt1 = new DateTimeOffset(2018, 1, 2, 12, 23, 45, TimeSpan.FromHours(-7));
Console.WriteLine($"{dt1}");
// → 2018/01/02 12:23:45 -07:00
var dt2 = new DateTimeOffset(2018, 1, 2, 12, 23, 45, TimeZoneInfo.Local.BaseUtcOffset);
Console.WriteLine($"{dt2}");
// → 2018/01/02 12:23:45 +09:00
- 足し算や引き算も、
TimeSpan
を考慮して計算してくれる。
var dt1 = new DateTimeOffset(2018, 1, 2, 12, 23, 45, TimeSpan.FromHours(-7));
var dt2 = new DateTimeOffset(2018, 1, 2, 12, 23, 45, TimeSpan.FromHours(+9));
Console.WriteLine(dt2 - dt1);
// → -16:00:00
-
==
の比較は、同一の日時であればTimeSpan
が異っていてもTrue
になる。
var dt1 = new DateTimeOffset(2018, 1, 2, 13, 23, 45, TimeSpan.FromHours(+1));
var dt2 = new DateTimeOffset(2018, 1, 2, 14, 23, 45, TimeSpan.FromHours(+2));
Console.WriteLine(dt1 == dt2); // → True
文字列から/への変換
プログラムで日時を扱うとき、一番多いのは現在時刻をどう取得するかですが、次に多いのは文字列から/への変換でしょう。
文字列 → DateTimeOffset
文字列 → DateTimeOffset
が、Parse()
メソッドとParseExact()
メソッドがあります。Parse()
は書式を自動的に判別して解析してくれるのですが、日本で一番使われているだろう、年月日を数値で並べた書式(たとえば、20180102131415)は、判別してくれませんでした。そのため、ParseExact()
を使うのですが、例のごとく引数に書式指定をします。
書式一覧については、コピペしてもしょうがないので、MSのドキュメントを参照してください。(そのうち表で書くかも)
- 年月日時分秒
var str = @"20180102131415";
var dt = DateTimeOffset.ParseExact(str, "yyyyMMddHHmmss", CultureInfo.CurrentCulture);
Console.WriteLine($"{dt.ToLocalTime()}");
// → 2018/01/02 13:14:15 +09:00
- 年月日時分秒+Z
var str = @"20180102131415Z";
var dt = DateTimeOffset.ParseExact(str, "yyyyMMddHHmmssZ", CultureInfo.CurrentCulture);
Console.WriteLine($"{dt.ToLocalTime()}");
// → 2018/01/02 22:14:15 +09:00
- 年月日時分秒+時差
var str = @"20180102131415-0700";
var dt = DateTimeOffset.ParseExact(str, "yyyyMMddHHmmsszzzzz", CultureInfo.CurrentCulture);
Console.WriteLine($"{dt.ToLocalTime()}");
// → 2018/01/03 5:14:15 +09:00
- 年月日時分秒+ミリ秒
var str = @"20180102131415.789-0700";
var dt = DateTimeOffset.ParseExact(str, "yyyyMMddHHmmss.fffzzzzz", CultureInfo.CurrentCulture);
ちなみに年が2桁の場合は、29(2000年扱い)と30(1900年扱い)で境界があります。
DateTimeOffset → 文字列
ToString()
の引数で、書式指定ができます。書式についてはMSのドキュメントを参照してください。
var dt = new DateTimeOffset(2018, 1, 2, 12, 23, 45, TimeZoneInfo.Local.BaseUtcOffset);
Console.WriteLine(dt.ToString("yyyy/MM/dd tt hh:mm:ss"));
// → 2018/01/02 午後 12:23:45
Console.WriteLine(dt.ToString("yyyy/MM/dd HH:mm:ss"));
// → 2018/01/02 12:23:45
※「午後12時」になってしまう。「午後0時」にはできない。
うるう秒の扱い
もはやUnixTimeとなんら関係もないですが、うるう秒についても補足をしておきます。
プログラム内での扱い
まずC#のプログラム内では、うるう秒を考慮する必要はありません。なぜなら、Windowsにはうるう秒が無いからです。DateTimeOffset
のドキュメントを見ても、秒がとりうる値は0~59になっています。(ちなみにWindowsの世界でうるう秒がないのは、多分MSがうるう秒導入反対の立場をとっているからだと思います)
時刻同期
では、実際うるう秒があった場合どうなるでしょううか。これはコンピューター間の時刻同期がどうなされるか、で決まるのですが、時刻同期はたいていNTPを使って行われます。そしてNTPはNTPサーバーとNTPクライアントの2つの立場があります。
- NTPクライアント
NTPサーバーに合わせるだけ。
-
NTPサーバー
- NTPサーバーがWindowsの場合:
Windowsの世界にはうるう秒が無いので、現実の世界でうるう秒入るとき、その前後の1秒の長さを調整して、うるう秒を挿入する。そのため「60秒」は存在しない。(ちなみに117時報サービスも、1秒の長さの調整でうるう秒を挿入します) - NTPサーバーがLinux系の場合:
NTPクライアントがWindowsの場合、うるう秒の挿入を無視する。次の時刻同期のタイミングで時間が合う。つまり、うるう秒の挿入~時刻同期の間まで、1秒ずれることになる。
- NTPサーバーがWindowsの場合:
時間の計算
1時間を加える、という処理を例にすると、Windowsの世界ではうるう秒が無いので常に1時間=3600秒であり、問題がありません。むしろJava(Linux系)の場合、3600秒と3601秒の場合があって、困ることになります(そのため、仕様書に「1時間後」ではなく「3600秒後」と書くようにする)。
うるう秒をまとめると…
WindowsとLinuxが混在しているシステムでは、1秒ずれることがあるかもしれない、ということを意味します。これはNTPサーバーがLinuxの場合だけ考慮すれば良い、ということでは無いです。
例えば、1時間後にファイルを送信する、という処理があった場合、Windowsでは3600秒後に送信するが、Linuxでは3601秒後を期待していた、ということがあるからです。
とは言うものの、現実に問題が発生するかと言われると、タイムスタンプが超重要なシステムでも無い限り、私の経験では問題が発生したことはありませんでした。ただし今後はどうなるかは分かりません。現在は、まだ1秒のずれがシステムに大きな問題を起こすほど、シビアな時間管理が必要ないのですが、未来については、もしかしたら1秒がシステムにとって重要になるかもしれません。
まとめ
-
DateTime
はオワコン。DateTimeOffset
を使え。 - UnixTimeの変換は、基準日(1970年1月1日0:00)を足し引きして、秒に変換すれば良い。
DateTimeOffset
の単位は100ナノ秒なので、10000000(=10*1000*1000)を掛けたり割ったりすれば良い。 - 時差は
DateTimeOffset
のコンストラクタ、TimeSpan
で指定。TimeSpan
に現地時間を指定するならTimeZoneInfo.Local.BaseUtcOffset
。 - 現在時刻は、
DateTimeOffset.Now
(現地時間)かDateTimeOffset.UtcNow
(標準時間)。 - 時間の計算は、
DateTimeOffset
を使っておけば、時差も考慮されるので、思考停止で使える。 - 文字列からの変換は
ParseExact()
、文字列への変換はToString()
。書式は例のごとく日付書式を引数に指定する。
参考文献
日付について書かれたサイトは数多いですが、多分このスライドショーが一番分かりやすいです。