LoginSignup
8
7

More than 5 years have passed since last update.

C#でUnixTimeを扱う(4.5以前)

Last updated at Posted at 2018-04-02
  • 現在時刻を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
  • コンストラクタで標準時間との時差を指定できる。現地時間を指定したい場合は、TimeSpanTimeZoneInfo.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秒ずれることになる。

時間の計算

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()。書式は例のごとく日付書式を引数に指定する。

参考文献

日付について書かれたサイトは数多いですが、多分このスライドショーが一番分かりやすいです。

8
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7