C# Tips | 日付・時間処理:ミリ秒切り捨て

C# C#
スポンサーリンク

はじめに 「ミリ秒切り捨て」は“誤差とノイズを消すための小技”

API のレスポンス時刻、ログのタイムスタンプ、DB に保存する日時。
こういうところで「ミリ秒までバラバラだと比較しづらい」「テストが安定しない」ということ、よくありますよね。

そこで使えるのが「ミリ秒切り捨て」です。
2026-02-18 20:10:30.4562026-02-18 20:10:30.000 にそろえるような処理です。

C# の DateTime は内部的に「ティック(ticks)」というもっと細かい単位で時間を持っているので、
ミリ秒切り捨ては「ティックをうまく丸める」ことで実現できます。


基本形:ミリ秒を 0 にして切り捨てる

一番素直な書き方(コンストラクタでミリ秒を 0 にする)

まずは「ミリ秒だけを 0 にする」一番ストレートな書き方です。

public static DateTime TruncateMilliseconds(DateTime dt)
{
    return new DateTime(
        dt.Year,
        dt.Month,
        dt.Day,
        dt.Hour,
        dt.Minute,
        dt.Second,
        0,
        dt.Kind);
}
C#

使い方の例です。

DateTime dt = new DateTime(2026, 2, 18, 20, 10, 30, 456); // 20:10:30.456

DateTime truncated = TruncateMilliseconds(dt);

Console.WriteLine(dt);        // 2026/02/18 20:10:30.456
Console.WriteLine(truncated); // 2026/02/18 20:10:30.000
C#

ここでのポイントは 2つです。

  • 年・月・日・時・分・秒はそのまま使い、ミリ秒だけ 0 にしている
  • dt.Kind をそのまま渡して、Utc / Local / Unspecified を壊さないようにしている

「とにかくミリ秒を消したい」だけなら、この書き方が一番分かりやすいです。


もう一歩踏み込む:Ticks を使って“より正確に”切り捨てる

DateTime は「ティック(100ナノ秒)」単位で時間を持っている

DateTime は内部的に「1ティック = 100ナノ秒」という単位で時間を表現しています。
1ミリ秒は 10,000 ティックです。

つまり、「ミリ秒切り捨て」は
「ティックを 10,000 の倍数に切り捨てる」と言い換えられます。

public static DateTime TruncateMillisecondsByTicks(DateTime dt)
{
    const long ticksPerMillisecond = TimeSpan.TicksPerMillisecond; // 10,000

    long ticks = dt.Ticks / ticksPerMillisecond * ticksPerMillisecond;

    return new DateTime(ticks, dt.Kind);
}
C#

使い方の例です。

DateTime dt = new DateTime(2026, 2, 18, 20, 10, 30, 456); // 20:10:30.456

DateTime truncated = TruncateMillisecondsByTicks(dt);

Console.WriteLine(truncated); // 2026/02/18 20:10:30.456  (※ここではミリ秒がちょうどのケース)
C#

new DateTime(年,月,日,時,分,秒,ミリ秒) で作った値は、
もともと「ミリ秒単位でピッタリのティック数」なので、
この例だと見た目は変わりません。

しかし、外部システムや計算結果によっては、
「ミリ秒表示は .456 だけど、内部のティックは .456 の少し手前/先」ということもあり得ます。
そういう“ミリ秒未満のブレ”も含めて切り捨てたいときは、
この「Ticks で丸める」方法がより厳密です。


実務での典型パターン:DB・JSON・比較のためにミリ秒を落とす

「保存するときにミリ秒を消す」ユーティリティ

よくあるのが、「DB に保存するときはミリ秒を持たせない」という方針です。
その場合、保存前に一度ミリ秒を切り捨てるユーティリティを通すようにします。

public static DateTime NormalizeForStorage(DateTime dt)
{
    return TruncateMilliseconds(dt);
}
C#

これをリポジトリや ORM の前段で必ず通すようにしておくと、
「あるデータは .123、別のデータは .124 で微妙に違うせいで比較に失敗する」
といった事故を防げます。

「比較前にミリ秒を無視する」パターン

「ミリ秒の違いは無視して“同じ時刻”とみなしたい」
というときも、比較前に切り捨ててしまうのがシンプルです。

public static bool EqualsIgnoringMilliseconds(DateTime x, DateTime y)
{
    return TruncateMilliseconds(x) == TruncateMilliseconds(y);
}
C#

これで、
2026-02-18 20:10:30.1232026-02-18 20:10:30.999
「同じ秒」として扱うことができます。


「なぜミリ秒を切り捨てるのか」を意識する

テストの安定性・ログの見やすさ・ビジネスルール

ミリ秒切り捨ては、単なる“見た目の揃え”ではなく、
実務では次のような意味を持ちます。

  • テストの安定性
    • 「今の時刻」を使うテストで、ミリ秒まで一致させるのは現実的ではない
    • ミリ秒を切り捨ててから比較することで、テストが安定する
  • ログ・監査の見やすさ
    • ミリ秒まで出ていると、人間が読むときにノイズになる
    • 秒単位にそろえることで、時系列が追いやすくなる
  • ビジネスルール
    • 「締め処理は秒単位まで」「ミリ秒は意味を持たない」
    • そういうルールなら、最初からミリ秒を捨てておいたほうが安全

「なぜミリ秒を切り捨てるのか」を自分の中で言語化しておくと、
どこでこのユーティリティを通すべきかが見えてきます。


実務での注意点:Kind を壊さないこと

Utc / Local / Unspecified をそのまま保つ

さきほどのコードで、すべて new DateTime(..., dt.Kind)new DateTime(ticks, dt.Kind) としていたのは、
DateTimeKind を壊さないためです。

もしここを省略して new DateTime(ticks) と書くと、
KindUnspecified になってしまいます。

その結果、

  • 本当は UTC のつもりだった値が「Kind 不明」になり
  • 後で ToLocalTime()ToUniversalTime() を呼んだときに、意図しない変換が起きる

といった事故につながります。

「丸めるときは必ず Kind を引き継ぐ」
これは日付・時刻ユーティリティを書くときの鉄則として、ぜひ覚えておいてください。


まとめ 「ミリ秒切り捨てユーティリティ」は“時間のノイズを消すフィルタ”

ミリ秒切り捨ては、小さなテクニックですが、
DB 保存、ログ、比較、テストなど、実務のあちこちで効いてきます。

押さえておきたいポイントは、次のイメージです。

  • 一番シンプルなのは「コンストラクタでミリ秒を 0 にする」書き方
  • より厳密にやるなら「Ticks を TimeSpan.TicksPerMillisecond で割って掛け直す」
  • 比較・保存・表示の前に一度ミリ秒を落としておくと、ブレやノイズを減らせる
  • DateTimeKind を必ず引き継ぐことが、タイムゾーン絡みのバグを防ぐうえでとても重要

ここまで理解できれば、
「なんとなくミリ秒が付いている DateTime」をそのまま使うのではなく、
“必要な精度にそろえた、実務で扱いやすい日時”としてコントロールできるようになります。

タイトルとURLをコピーしました