日時

【C#】DateTimeとTimeSpanで時間を簡単に加算・減算する方法と実用サンプル

C#で日時や時間の計算をする最短手段は、DateTimeとTimeSpanを組み合わせる方法です。

DateTimeはAddYearsなどのAdd系メソッドで正負の値を渡すだけで加算も減算も完結し、時間差はSubtractでTimeSpanを取得できます。

TimeSpan同士は演算子+-で扱え、日数や分秒を柔軟に加減できます。

DateTimeの基礎知識

DateTimeとは何か

C#のDateTime型は、日付と時刻を表現するための構造体です。

これを使うことで、特定の日時を扱ったり、日時の計算を行ったりできます。

DateTimeは日付と時刻の両方を持ち、年、月、日、時、分、秒、ミリ秒までの情報を保持します。

構造と内部表現 ticks

DateTimeは内部的に「ticks(ティック)」という単位で日時を管理しています。

1ティックは100ナノ秒(1秒の1千万分の1)に相当し、DateTimeの最小単位です。

DateTimeの値は、0001年1月1日午前0時0分0秒(西暦1年1月1日)からの経過ティック数として表現されています。

このため、DateTimeは非常に高精度な日時を扱えます。

例えば、DateTime.Ticksプロパティを使うと、その日時が何ティック目かを取得できます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2023, 6, 15, 12, 30, 45, 123);
        Console.WriteLine($"Ticks: {dt.Ticks}");
    }
}
Ticks: 638224269451230123

このように、Ticksは日時の内部表現を示し、日時の比較や計算の基礎となっています。

ローカル時刻とUTC

DateTimeは日時の種類を示すKindプロパティを持っています。

Kindは以下の3種類です。

  • DateTimeKind.Local:ローカルタイム(コンピュータのタイムゾーンに基づく)
  • DateTimeKind.Utc:協定世界時(UTC)
  • DateTimeKind.Unspecified:指定なし(タイムゾーン情報が不明)

ローカル時刻は、ユーザーの環境に合わせた日時を表します。

一方、UTCは世界共通の基準時刻で、タイムゾーンの影響を受けません。

日時の比較や保存、通信時にはUTCを使うことが多いです。

DateTimeToLocalTime()ToUniversalTime()メソッドを使うと、ローカル時刻とUTCの変換ができます。

using System;
class Program
{
    static void Main()
    {
        DateTime utcNow = DateTime.UtcNow;
        DateTime localNow = utcNow.ToLocalTime();
        Console.WriteLine($"UTC: {utcNow}");
        Console.WriteLine($"Local: {localNow}");
    }
}
UTC: 2025/05/07 1:42:43
Local: 2025/05/07 10:42:43

このように、DateTimeは日時の種類を意識して使うことが重要です。

現在日時の取得

DateTime.Now

DateTime.Nowは、現在のローカル日時を取得するプロパティです。

コンピュータの設定されたタイムゾーンに基づく日時が返されます。

例えば、ユーザーのPCが日本標準時(JST)なら、日本時間の現在日時が取得できます。

using System;
class Program
{
    static void Main()
    {
        DateTime now = DateTime.Now;
        Console.WriteLine($"現在のローカル日時: {now}");
    }
}
現在のローカル日時: 2025/05/07 10:42:47

DateTime.Nowは、ユーザーの環境に依存する日時を扱いたい場合に便利です。

DateTime.UtcNow

DateTime.UtcNowは、現在の協定世界時(UTC)を取得するプロパティです。

タイムゾーンの影響を受けず、世界共通の基準時刻を返します。

using System;
class Program
{
    static void Main()
    {
        DateTime utcNow = DateTime.UtcNow;
        Console.WriteLine($"現在のUTC日時: {utcNow}");
    }
}
現在のUTC日時: 2025/05/07 1:42:51

UTCは、サーバー間の日時同期やログの記録、国際的な日時管理に適しています。

指定日時の作成

コンストラクタ

DateTimeは複数のコンストラクタを持ち、年、月、日、時、分、秒、ミリ秒を指定して日時を作成できます。

最も基本的な使い方は、年・月・日を指定する方法です。

using System;
class Program
{
    static void Main()
    {
        // 2024年6月15日午前10時30分を作成
        DateTime dt = new DateTime(2024, 6, 15, 10, 30, 0);
        Console.WriteLine($"指定日時: {dt}");
    }
}
指定日時: 2024/06/15 10:30:00

また、DateTimeのコンストラクタにはDateTimeKindを指定できるオーバーロードもあります。

これにより、作成した日時がローカルかUTCかを明示できます。

DateTime utcDate = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);

ParseとTryParse

文字列からDateTimeを作成するには、ParseTryParseメソッドを使います。

Parseは文字列が正しい日時形式でない場合に例外を投げますが、TryParseは失敗しても例外を投げず、戻り値で成功・失敗を判定できます。

using System;
class Program
{
    static void Main()
    {
        string dateStr = "2024-06-15 10:30:00";
        // Parseは例外が発生する可能性あり
        DateTime dt1 = DateTime.Parse(dateStr);
        Console.WriteLine($"Parse結果: {dt1}");
        // TryParseは安全に変換可能か判定できる
        if (DateTime.TryParse(dateStr, out DateTime dt2))
        {
            Console.WriteLine($"TryParse成功: {dt2}");
        }
        else
        {
            Console.WriteLine("TryParse失敗");
        }
    }
}
Parse結果: 2024/06/15 10:30:00
TryParse成功: 2024/06/15 10:30:00

TryParseはユーザー入力や外部データの日時変換に適しています。

ParseExactとTryParseExact

日時の文字列形式が決まっている場合は、ParseExactTryParseExactを使うと、指定したフォーマットに厳密に従って変換できます。

これにより、曖昧な解釈を防げます。

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        string dateStr = "2024/06/15 10:30:00";
        string format = "yyyy/MM/dd HH:mm:ss";
        // ParseExactはフォーマットが合わないと例外を投げる
        DateTime dt1 = DateTime.ParseExact(dateStr, format, CultureInfo.InvariantCulture);
        Console.WriteLine($"ParseExact結果: {dt1}");
        // TryParseExactは安全に判定可能
        if (DateTime.TryParseExact(dateStr, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime dt2))
        {
            Console.WriteLine($"TryParseExact成功: {dt2}");
        }
        else
        {
            Console.WriteLine("TryParseExact失敗");
        }
    }
}
ParseExact結果: 2024/06/15 10:30:00
TryParseExact成功: 2024/06/15 10:30:00

ParseExact系は、ログファイルの日時解析やAPIの日時フォーマット処理に役立ちます。

TimeSpanの基礎知識

TimeSpanとは何か

TimeSpanは、時間の長さや時間間隔を表す構造体です。

日時そのものではなく、「どれくらいの時間が経過したか」や「2つの日時の差」を扱う際に使います。

例えば、1時間30分の作業時間や、2日間の期間などを表現できます。

表される単位

TimeSpanは内部的に「ticks(ティック)」で時間を管理しています。

1ティックは100ナノ秒(1秒の1千万分の1)です。

TimeSpanは日、時間、分、秒、ミリ秒、ティック単位で時間を表現可能です。

時間の単位は以下のように分解できます。

  • 1日 = 24時間
  • 1時間 = 60分
  • 1分 = 60秒
  • 1秒 = 1000ミリ秒
  • 1ミリ秒 = 10,000ティック

このため、TimeSpanは非常に細かい時間間隔も正確に表せます。

TimeSpanの生成方法

コンストラクタ

TimeSpanは複数のコンストラクタを持ち、日、時間、分、秒、ミリ秒を指定して生成できます。

代表的なコンストラクタは以下の通りです。

  • TimeSpan(int hours, int minutes, int seconds)
  • TimeSpan(int days, int hours, int minutes, int seconds)
  • TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds)

例として、1日2時間30分15秒を表すTimeSpanを作成するコードです。

using System;
class Program
{
    static void Main()
    {
        TimeSpan ts = new TimeSpan(1, 2, 30, 15);
        Console.WriteLine($"TimeSpan: {ts}");
    }
}
TimeSpan: 1.02:30:15

この表示は「1日 2時間 30分 15秒」を意味します。

From系メソッド

TimeSpanには、数値から時間間隔を生成する静的メソッドが用意されています。

これらは単位を指定して簡単にTimeSpanを作成できます。

主なFromメソッドは以下の通りです。

メソッド名説明
FromDays(double)日数から生成
FromHours(double)時間数から生成
FromMinutes(double)分数から生成
FromSeconds(double)秒数から生成
FromMilliseconds(double)ミリ秒数から生成
FromTicks(long)ティック数から生成

例えば、1.5時間(1時間30分)をTimeSpanで表す場合は以下のように書けます。

using System;
class Program
{
    static void Main()
    {
        TimeSpan ts = TimeSpan.FromHours(1.5);
        Console.WriteLine($"1.5時間のTimeSpan: {ts}");
    }
}
1.5時間のTimeSpan: 01:30:00

Fromメソッドは小数も受け付けるため、細かい時間指定が可能です。

TimeSpanの主要プロパティ

Days, Hours, Minutes

TimeSpanのインスタンスは、日、時間、分、秒、ミリ秒の各部分を個別に取得できます。

代表的なプロパティは以下の通りです。

  • Days:日数部分(整数)
  • Hours:時間部分(0~23の整数)
  • Minutes:分部分(0~59の整数)
  • Seconds:秒部分(0~59の整数)
  • Milliseconds:ミリ秒部分(0~999の整数)

これらはTimeSpanの「各単位の余り」を表します。

例えば、TimeSpanが1日2時間30分の場合、Daysは1、Hoursは2、Minutesは30となります。

using System;
class Program
{
    static void Main()
    {
        TimeSpan ts = new TimeSpan(1, 2, 30, 15);
        Console.WriteLine($"Days: {ts.Days}");
        Console.WriteLine($"Hours: {ts.Hours}");
        Console.WriteLine($"Minutes: {ts.Minutes}");
        Console.WriteLine($"Seconds: {ts.Seconds}");
    }
}
Days: 1
Hours: 2
Minutes: 30
Seconds: 15

Total系プロパティの違い

TimeSpanには、全体の時間を単位ごとに小数で表すTotal系プロパティもあります。

これらは時間全体を指定単位で表現し、小数点以下も含みます。

主なTotal系プロパティは以下の通りです。

  • TotalDays:全体の時間を日数で表す(例:1.1042日)
  • TotalHours:全体の時間を時間で表す(例:26.5時間)
  • TotalMinutes:全体の時間を分で表す
  • TotalSeconds:全体の時間を秒で表す
  • TotalMilliseconds:全体の時間をミリ秒で表す

例えば、1日2時間30分15秒のTimeSpanTotalHoursは26.5042時間となります。

using System;
class Program
{
    static void Main()
    {
        TimeSpan ts = new TimeSpan(1, 2, 30, 15);
        Console.WriteLine($"TotalDays: {ts.TotalDays}");
        Console.WriteLine($"TotalHours: {ts.TotalHours}");
        Console.WriteLine($"TotalMinutes: {ts.TotalMinutes}");
    }
}
TotalDays: 1.10434027777778
TotalHours: 26.5041666666667
TotalMinutes: 1590.25

Total系は、時間の合計を単位換算して計算したい場合に便利です。

DaysHoursはあくまで「部分的な値」であるのに対し、Total系は全体の時間を表す点が異なります。

用途に応じて使い分けてください。

DateTimeで時間を加算する方法

Add系メソッド一覧

DateTime型には、日時に特定の時間単位を加算するためのAdd系メソッドが豊富に用意されています。

これらのメソッドは元の日時を変更せず、新しい日時を返します。

加算したい単位に応じて使い分けることができます。

AddYears

AddYears(int value)は、指定した年数を日時に加算します。

負の値を渡すと減算になります。

うるう年の調整も自動で行われます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2020, 2, 29);
        DateTime newDt = dt.AddYears(1);
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"1年加算後: {newDt}");
    }
}
元の日時: 2020/02/29 00:00:00
1年加算後: 2021/02/28 00:00:00

うるう年の2月29日から1年加算すると、翌年は2月28日になります。

AddMonths

AddMonths(int value)は、指定した月数を加算します。

こちらも負の値で減算可能です。

月末の日付調整も自動で行われます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2024, 1, 31);
        DateTime newDt = dt.AddMonths(1);
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"1ヶ月加算後: {newDt}");
    }
}
元の日時: 2024/01/31 00:00:00
1ヶ月加算後: 2024/02/29 00:00:00

1月31日に1ヶ月加算すると、2月29日(うるう年の場合)に調整されます。

AddDays

AddDays(double value)は、日数を加算します。

小数も指定でき、小数部分は時間に換算されます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2024, 6, 15, 12, 0, 0);
        DateTime newDt = dt.AddDays(1.5);
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"1.5日加算後: {newDt}");
    }
}
元の日時: 2024/06/15 12:00:00
1.5日加算後: 2024/06/17 00:00:00

1.5日加算すると、1日と12時間が加わります。

AddHours

AddHours(double value)は、時間単位で加算します。

こちらも小数指定が可能です。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2024, 6, 15, 8, 0, 0);
        DateTime newDt = dt.AddHours(3.25);
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"3.25時間加算後: {newDt}");
    }
}
元の日時: 2024/06/15 08:00:00
3.25時間加算後: 2024/06/15 11:15:00

3.25時間は3時間15分に相当します。

AddMinutes

AddMinutes(double value)は、分単位で加算します。

小数指定で秒単位の加算も可能です。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2024, 6, 15, 10, 0, 0);
        DateTime newDt = dt.AddMinutes(90.5);
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"90.5分加算後: {newDt}");
    }
}
元の日時: 2024/06/15 10:00:00
90.5分加算後: 2024/06/15 11:30:30

90.5分は1時間30分30秒に相当します。

AddSeconds

AddSeconds(double value)は、秒単位で加算します。

小数指定でミリ秒単位の加算も可能です。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2024, 6, 15, 10, 0, 0);
        DateTime newDt = dt.AddSeconds(45.75);
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"45.75秒加算後: {newDt}");
    }
}
元の日時: 2024/06/15 10:00:00
45.75秒加算後: 2024/06/15 10:00:45.7500000

45.75秒は45秒750ミリ秒に相当します。

AddMilliseconds

AddMilliseconds(double value)は、ミリ秒単位で加算します。

小数指定でマイクロ秒単位の加算も可能ですが、精度はティック単位(100ナノ秒)までです。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2024, 6, 15, 10, 0, 0);
        DateTime newDt = dt.AddMilliseconds(1500.5);
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"1500.5ミリ秒加算後: {newDt}");
    }
}
元の日時: 2024/06/15 10:00:00
1500.5ミリ秒加算後: 2024/06/15 10:00:01.5005000

1500.5ミリ秒は1秒500ミリ秒500マイクロ秒に相当します。

AddTicksによる最小単位加算

AddTicks(long value)は、DateTimeの最小単位であるティック単位で加算します。

1ティックは100ナノ秒なので、非常に細かい時間調整が可能です。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2024, 6, 15, 10, 0, 0);
        DateTime newDt = dt.AddTicks(5000); // 5000ティック = 5000 * 100ns = 0.5ms
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"5000ティック加算後: {newDt}");
    }
}
元の日時: 2024/06/15 10:00:00
5000ティック加算後: 2024/06/15 10:00:00.0005000

このように、ミリ秒よりもさらに細かい単位で日時を調整したい場合に使います。

複数単位を組み合わせた加算

複数の時間単位を同時に加算したい場合は、Add系メソッドを連続して呼び出すか、TimeSpanを使って加算する方法があります。

Add系メソッドの連続呼び出し例

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2024, 6, 15, 8, 0, 0);
        DateTime newDt = dt.AddDays(1).AddHours(2).AddMinutes(30);
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"1日2時間30分加算後: {newDt}");
    }
}
元の日時: 2024/06/15 08:00:00
1日2時間30分加算後: 2024/06/16 10:30:00

TimeSpanを使った加算例

TimeSpanで複数単位をまとめて表現し、DateTimeに加算する方法もあります。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2024, 6, 15, 8, 0, 0);
        TimeSpan ts = new TimeSpan(1, 2, 30, 0); // 1日2時間30分
        DateTime newDt = dt + ts;
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"TimeSpanで1日2時間30分加算後: {newDt}");
    }
}
元の日時: 2024/06/15 08:00:00
TimeSpanで1日2時間30分加算後: 2024/06/16 10:30:00

TimeSpanを使うと、加算したい時間をまとめて管理でき、コードの可読性も向上します。

用途に応じて使い分けてください。

DateTimeで時間を減算する方法

Add系メソッドに負の値を渡す

DateTimeAdd系メソッドは、加算だけでなく負の値を渡すことで減算にも使えます。

例えば、AddDays(-3)は3日前の日付を返します。

元の日時は変更されず、新しい日時が返される点に注意してください。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2024, 6, 15, 12, 0, 0);
        DateTime threeDaysBefore = dt.AddDays(-3);
        DateTime twoHoursBefore = dt.AddHours(-2);
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"3日前: {threeDaysBefore}");
        Console.WriteLine($"2時間前: {twoHoursBefore}");
    }
}
元の日時: 2024/06/15 12:00:00
3日前: 2024/06/12 12:00:00
2時間前: 2024/06/15 10:00:00

この方法はシンプルで直感的に使えますが、複数単位の減算をまとめて行いたい場合はTimeSpanを使うほうが便利です。

Subtract(TimeSpan)の活用

DateTimeSubtract(TimeSpan)メソッドは、指定した時間間隔を減算した新しい日時を返します。

TimeSpanを使うことで、日数や時間、分など複数単位をまとめて減算できます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2024, 6, 15, 12, 0, 0);
        TimeSpan ts = new TimeSpan(1, 3, 30, 0); // 1日3時間30分
        DateTime newDt = dt.Subtract(ts);
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"1日3時間30分減算後: {newDt}");
    }
}
元の日時: 2024/06/15 12:00:00
1日3時間30分減算後: 2024/06/14 08:30:00

Subtract(TimeSpan)は複数単位の減算を一度に行いたい場合に便利です。

DateTime同士の差分取得

dt1 – dt2 によるTimeSpan

DateTime同士の減算は、-演算子を使って簡単に行えます。

dt1 - dt2の結果はTimeSpan型で、2つの日時の差分を表します。

using System;
class Program
{
    static void Main()
    {
        DateTime dt1 = new DateTime(2024, 6, 15, 12, 0, 0);
        DateTime dt2 = new DateTime(2024, 6, 14, 8, 30, 0);
        TimeSpan diff = dt1 - dt2;
        Console.WriteLine($"日時1: {dt1}");
        Console.WriteLine($"日時2: {dt2}");
        Console.WriteLine($"差分: {diff}");
        Console.WriteLine($"差分の合計時間(時間単位): {diff.TotalHours}");
    }
}
日時1: 2024/06/15 12:00:00
日時2: 2024/06/14 08:30:00
差分: 1.03:30:00
差分の合計時間(時間単位): 27.5

このように、日時の差分をTimeSpanで取得し、日数や時間、分などに分解して利用できます。

Subtract(DateTime) の違い

DateTimeSubtract(DateTime)メソッドも、引数に指定した日時との差分をTimeSpanで返します。

-演算子とほぼ同じ動作ですが、メソッド呼び出し形式である点が異なります。

using System;
class Program
{
    static void Main()
    {
        DateTime dt1 = new DateTime(2024, 6, 15, 12, 0, 0);
        DateTime dt2 = new DateTime(2024, 6, 14, 8, 30, 0);
        TimeSpan diff = dt1.Subtract(dt2);
        Console.WriteLine($"日時1: {dt1}");
        Console.WriteLine($"日時2: {dt2}");
        Console.WriteLine($"差分: {diff}");
    }
}
日時1: 2024/06/15 12:00:00
日時2: 2024/06/14 08:30:00
差分: 1.03:30:00

-演算子とSubtract(DateTime)は機能的に同等で、好みやコードの可読性に応じて使い分けてください。

TimeSpan同士の計算

演算子 + と –

TimeSpan同士の加算や減算は、+および-演算子を使って簡単に行えます。

これらの演算子は新しいTimeSpanを返し、元の値は変更されません。

using System;
class Program
{
    static void Main()
    {
        TimeSpan ts1 = new TimeSpan(1, 2, 30, 0); // 1日2時間30分
        TimeSpan ts2 = new TimeSpan(0, 3, 45, 15); // 3時間45分15秒
        TimeSpan sum = ts1 + ts2;
        TimeSpan diff = ts1 - ts2;
        Console.WriteLine($"ts1: {ts1}");
        Console.WriteLine($"ts2: {ts2}");
        Console.WriteLine($"加算結果: {sum}");
        Console.WriteLine($"減算結果: {diff}");
    }
}
ts1: 1.02:30:00
ts2: 03:45:15
加算結果: 1.06:15:15
減算結果: 22:44:45

加算結果は1日6時間15分15秒、減算結果は22時間44分45秒となります。

TimeSpanの演算は直感的で使いやすいです。

Add と Subtract メソッド

TimeSpanにはAdd(TimeSpan)Subtract(TimeSpan)メソッドもあります。

これらは演算子と同様の機能を持ち、メソッドチェーンやラムダ式での利用に便利です。

using System;
class Program
{
    static void Main()
    {
        TimeSpan ts1 = new TimeSpan(0, 5, 0, 0); // 5時間
        TimeSpan ts2 = new TimeSpan(0, 1, 30, 0); // 1時間30分
        TimeSpan added = ts1.Add(ts2);
        TimeSpan subtracted = ts1.Subtract(ts2);
        Console.WriteLine($"Add結果: {added}");
        Console.WriteLine($"Subtract結果: {subtracted}");
    }
}
Add結果: 06:30:00
Subtract結果: 03:30:00

AddSubtractは演算子と同じ結果を返しますが、メソッド形式なのでコードの可読性や柔軟性を高める場面で役立ちます。

乗算と除算によるスケール

TimeSpanは乗算や除算のメソッドを標準で持っていませんが、時間間隔をスケール(拡大・縮小)したい場合は、自分で計算を行う必要があります。

具体的には、Ticksプロパティを使ってティック単位で計算し、新しいTimeSpanを生成します。

TimeSpan.Multiply 相当の実装例

以下は、TimeSpanを倍数で乗算する拡張メソッドの例です。

using System;
static class TimeSpanExtensions
{
    public static TimeSpan Multiply(this TimeSpan timeSpan, double factor)
    {
        long ticks = (long)(timeSpan.Ticks * factor);
        return new TimeSpan(ticks);
    }
}
class Program
{
    static void Main()
    {
        TimeSpan ts = new TimeSpan(1, 2, 0, 0); // 1日2時間
        TimeSpan multiplied = ts.Multiply(1.5); // 1.5倍
        Console.WriteLine($"元のTimeSpan: {ts}");
        Console.WriteLine($"1.5倍したTimeSpan: {multiplied}");
    }
}
元のTimeSpan: 1.02:00:00
1.5倍したTimeSpan: 1.15:00:00

この例では、1日2時間の1.5倍、すなわち1日15時間が計算されています。

Divide の代替手段

TimeSpanの除算は、Ticksを使って割り算を行い、新しいTimeSpanを作成する方法が一般的です。

以下は除算の例です。

using System;
static class TimeSpanExtensions
{
    public static TimeSpan Divide(this TimeSpan timeSpan, double divisor)
    {
        if (divisor == 0)
            throw new DivideByZeroException("除数は0にできません。");
        long ticks = (long)(timeSpan.Ticks / divisor);
        return new TimeSpan(ticks);
    }
}
class Program
{
    static void Main()
    {
        TimeSpan ts = new TimeSpan(2, 0, 0); // 2時間
        TimeSpan divided = ts.Divide(4); // 4で割る
        Console.WriteLine($"元のTimeSpan: {ts}");
        Console.WriteLine($"4で割ったTimeSpan: {divided}");
    }
}
元のTimeSpan: 02:00:00
4で割ったTimeSpan: 00:30:00

このように、MultiplyDivideの拡張メソッドを用意することで、TimeSpanのスケール操作が簡単に行えます。

標準APIにない機能を補う形で活用してください。

DateTimeとTimeSpanの組み合わせテクニック

予定日時の計算シナリオ

翌営業日の取得

ビジネスシーンでは、翌営業日を計算することがよくあります。

土日や祝日を除外して、次の営業日を求めるにはDateTimeTimeSpanを組み合わせて処理します。

ここでは土日を休日と仮定し、翌営業日を取得する例を示します。

using System;
class Program
{
    static DateTime GetNextBusinessDay(DateTime date)
    {
        DateTime nextDay = date.AddDays(1);
        // 土曜日の場合は2日後(月曜日)にする
        if (nextDay.DayOfWeek == DayOfWeek.Saturday)
        {
            nextDay = nextDay.AddDays(2);
        }
        // 日曜日の場合は1日後(月曜日)にする
        else if (nextDay.DayOfWeek == DayOfWeek.Sunday)
        {
            nextDay = nextDay.AddDays(1);
        }
        return nextDay;
    }
    static void Main()
    {
        DateTime today = new DateTime(2024, 6, 14); // 金曜日
        DateTime nextBusinessDay = GetNextBusinessDay(today);
        Console.WriteLine($"今日: {today:yyyy/MM/dd} ({today.DayOfWeek})");
        Console.WriteLine($"翌営業日: {nextBusinessDay:yyyy/MM/dd} ({nextBusinessDay.DayOfWeek})");
        today = new DateTime(2024, 6, 15); // 土曜日
        nextBusinessDay = GetNextBusinessDay(today);
        Console.WriteLine($"今日: {today:yyyy/MM/dd} ({today.DayOfWeek})");
        Console.WriteLine($"翌営業日: {nextBusinessDay:yyyy/MM/dd} ({nextBusinessDay.DayOfWeek})");
    }
}
今日: 2024/06/14 (Friday)
翌営業日: 2024/06/17 (Monday)
今日: 2024/06/15 (Saturday)
翌営業日: 2024/06/17 (Monday)

この例では、AddDays(1)で翌日を取得し、土曜なら2日後、日曜なら1日後に調整しています。

祝日を考慮する場合は、祝日リストを用意して判定を追加すると良いでしょう。

有効期限の判定シナリオ

商品の有効期限やライセンスの期限判定には、DateTimeTimeSpanの組み合わせが役立ちます。

例えば、購入日から30日間の有効期限を計算し、現在日時と比較して期限切れかどうかを判定します。

using System;
class Program
{
    static bool IsExpired(DateTime purchaseDate, TimeSpan validPeriod)
    {
        DateTime expiryDate = purchaseDate + validPeriod;
        return DateTime.Now > expiryDate;
    }
    static void Main()
    {
        DateTime purchaseDate = new DateTime(2024, 5, 1);
        TimeSpan validPeriod = TimeSpan.FromDays(30);
        bool expired = IsExpired(purchaseDate, validPeriod);
        Console.WriteLine($"購入日: {purchaseDate:yyyy/MM/dd}");
        Console.WriteLine($"有効期限: {purchaseDate.Add(validPeriod):yyyy/MM/dd}");
        Console.WriteLine($"現在日時: {DateTime.Now:yyyy/MM/dd}");
        Console.WriteLine($"期限切れ: {(expired ? "はい" : "いいえ")}");
    }
}
購入日: 2024/05/01
有効期限: 2024/05/31
現在日時: 2025/05/07
期限切れ: はい

このように、TimeSpanで有効期間を表現し、DateTimeの加算と比較で期限判定を行います。

TimeSpanを使うことで、日数だけでなく時間や分単位の有効期限も柔軟に扱えます。

勤怠時間の集計シナリオ

従業員の勤怠時間を集計する際、出勤時刻と退勤時刻の差分をTimeSpanで計算し、日々の勤務時間を合計します。

複数日の勤務時間を合算することで、月間や週単位の勤務時間を求められます。

using System;
class Program
{
    static void Main()
    {
        // 出勤・退勤時刻の例(複数日)
        DateTime[] clockInTimes = {
            new DateTime(2024, 6, 10, 9, 0, 0),
            new DateTime(2024, 6, 11, 9, 15, 0),
            new DateTime(2024, 6, 12, 8, 50, 0)
        };
        DateTime[] clockOutTimes = {
            new DateTime(2024, 6, 10, 18, 0, 0),
            new DateTime(2024, 6, 11, 17, 45, 0),
            new DateTime(2024, 6, 12, 18, 10, 0)
        };
        TimeSpan totalWorkTime = TimeSpan.Zero;
        for (int i = 0; i < clockInTimes.Length; i++)
        {
            TimeSpan workTime = clockOutTimes[i] - clockInTimes[i];
            Console.WriteLine($"勤務日 {clockInTimes[i]:yyyy/MM/dd} の勤務時間: {workTime}");
            totalWorkTime += workTime;
        }
        Console.WriteLine($"合計勤務時間: {totalWorkTime}");
        Console.WriteLine($"合計勤務時間(時間単位): {totalWorkTime.TotalHours:F2} 時間");
    }
}
勤務日 2024/06/10 の勤務時間: 09:00:00
勤務日 2024/06/11 の勤務時間: 08:30:00
勤務日 2024/06/12 の勤務時間: 09:20:00
合計勤務時間: 1.02:50:00
合計勤務時間(時間単位): 26.83 時間

この例では、DateTimeの差分で日ごとの勤務時間を計算し、TimeSpanの加算で合計勤務時間を求めています。

TotalHoursプロパティを使うと、時間単位の合計も簡単に取得できます。

実用サンプル集

シフト表自動生成

従業員のシフト表を自動生成する際、DateTimeTimeSpanを活用して勤務開始時刻や終了時刻を計算します。

以下は、1週間分のシフトを日ごとに8時間勤務で作成するサンプルです。

using System;
class Program
{
    static void Main()
    {
        DateTime startDate = new DateTime(2024, 6, 17); // シフト開始日(月曜日)
        TimeSpan shiftDuration = TimeSpan.FromHours(8); // 1日の勤務時間
        Console.WriteLine("シフト表(1週間)");
        for (int i = 0; i < 7; i++)
        {
            DateTime workDay = startDate.AddDays(i);
            DateTime shiftStart = workDay.AddHours(9); // 9時出勤
            DateTime shiftEnd = shiftStart + shiftDuration;
            Console.WriteLine($"{workDay:yyyy/MM/dd} ({workDay.DayOfWeek}): {shiftStart:HH:mm}{shiftEnd:HH:mm}");
        }
    }
}
シフト表(1週間)
2024/06/17 (Monday): 09:00 ~ 17:00
2024/06/18 (Tuesday): 09:00 ~ 17:00
2024/06/19 (Wednesday): 09:00 ~ 17:00
2024/06/20 (Thursday): 09:00 ~ 17:00
2024/06/21 (Friday): 09:00 ~ 17:00
2024/06/22 (Saturday): 09:00 ~ 17:00
2024/06/23 (Sunday): 09:00 ~ 17:00

このように、AddDaysで日付を進め、TimeSpanで勤務時間を加算してシフトの終了時刻を計算しています。

休日や特別休暇を考慮する場合は条件分岐を追加してください。

24時間制限タイマー

24時間以内に処理を完了させる必要があるタイマー処理では、DateTimeTimeSpanを使って残り時間を計算し、制限時間を超えたかどうかを判定します。

using System;
class Program
{
    static void Main()
    {
        DateTime startTime = DateTime.Now;
        TimeSpan limit = TimeSpan.FromHours(24);
        // 処理のシミュレーション(ここでは5時間経過と仮定)
        DateTime currentTime = startTime.AddHours(5);
        TimeSpan elapsed = currentTime - startTime;
        TimeSpan remaining = limit - elapsed;
        Console.WriteLine($"開始時刻: {startTime}");
        Console.WriteLine($"現在時刻: {currentTime}");
        Console.WriteLine($"経過時間: {elapsed}");
        Console.WriteLine($"残り時間: {remaining}");
        if (elapsed > limit)
        {
            Console.WriteLine("24時間の制限時間を超えました。");
        }
        else
        {
            Console.WriteLine("まだ制限時間内です。");
        }
    }
}
開始時刻: 2025/05/07 10:44:26
現在時刻: 2025/05/07 15:44:26
経過時間: 05:00:00
残り時間: 19:00:00
まだ制限時間内です。

この例では、開始時刻からの経過時間と残り時間を計算し、24時間の制限を超えたかどうかを判定しています。

データのロールオーバー処理

日付をまたぐデータ処理(ロールオーバー)では、DateTimeTimeSpanを使って処理の切り替えタイミングを判定します。

例えば、深夜0時をまたいだら新しい日付の処理に切り替えるケースです。

using System;
class Program
{
    static void Main()
    {
        DateTime lastProcessTime = new DateTime(2024, 6, 14, 23, 50, 0);
        DateTime currentTime = new DateTime(2024, 6, 15, 0, 10, 0);
        // 日付が変わったか判定
        if (currentTime.Date > lastProcessTime.Date)
        {
            Console.WriteLine("日付が変わったため、ロールオーバー処理を実行します。");
        }
        else
        {
            Console.WriteLine("同じ日付のため、通常処理を継続します。");
        }
    }
}
日付が変わったため、ロールオーバー処理を実行します。

DateTime.Dateプロパティで日付部分だけを比較し、日付の切り替わりを検出しています。

これにより、日跨ぎの処理を安全に行えます。

スケジュール通知機能

スケジュール通知では、現在時刻と通知予定時刻の差分を計算し、通知タイミングを判定します。

TimeSpanを使って残り時間を求めることで、柔軟な通知制御が可能です。

using System;
class Program
{
    static void Main()
    {
        DateTime now = DateTime.Now;
        DateTime notifyTime = now.AddMinutes(10); // 10分後に通知予定
        TimeSpan timeUntilNotify = notifyTime - now;
        Console.WriteLine($"現在時刻: {now}");
        Console.WriteLine($"通知予定時刻: {notifyTime}");
        Console.WriteLine($"通知までの残り時間: {timeUntilNotify}");
        if (timeUntilNotify <= TimeSpan.Zero)
        {
            Console.WriteLine("通知時間です。");
        }
        else
        {
            Console.WriteLine("まだ通知時間ではありません。");
        }
    }
}
現在時刻: 2025/05/07 10:44:34
通知予定時刻: 2025/05/07 10:54:34
通知までの残り時間: 00:10:00
まだ通知時間ではありません。

このサンプルでは、通知予定時刻までの残り時間を計算し、通知すべきかどうかを判定しています。

残り時間がゼロ以下になったら通知を実行する仕組みです。

注意点と落とし穴

夏時間とタイムゾーン

夏時間(サマータイム)は、一部の地域で季節に応じて時計を1時間進めたり戻したりする制度です。

DateTimeを扱う際に夏時間の影響を考慮しないと、意図しない日時のズレや計算ミスが発生します。

例えば、夏時間の開始や終了時刻にまたがる日時の加算・減算では、1時間の差異が生じることがあります。

DateTime自体はタイムゾーンの詳細な情報を持たず、KindプロパティでローカルかUTCかを区別するだけなので、夏時間の自動調整はOSのタイムゾーン設定に依存します。

Kind プロパティの罠

DateTimeKindプロパティは、日時がローカルDateTimeKind.Local、UTCDateTimeKind.Utc、または不明DateTimeKind.Unspecifiedのいずれかを示します。

しかし、このプロパティの扱いを誤ると、夏時間の変換や比較で問題が起きやすいです。

例えば、DateTimeをUTCとして扱うべきところをローカルとして扱うと、夏時間の影響で1時間のズレが生じることがあります。

また、Unspecifiedの日時はタイムゾーン情報がないため、変換時に誤った解釈をされる可能性があります。

using System;
class Program
{
    static void Main()
    {
        DateTime localTime = new DateTime(2024, 3, 10, 2, 30, 0, DateTimeKind.Local);
        DateTime utcTime = localTime.ToUniversalTime();
        Console.WriteLine($"ローカル時間: {localTime} ({localTime.Kind})");
        Console.WriteLine($"UTC時間: {utcTime} ({utcTime.Kind})");
    }
}
ローカル時間: 2024/03/10 2:30:00 (Local)
UTC時間: 2024/03/09 17:30:00 (Utc)

夏時間の切り替え時刻付近では、ToUniversalTime()の結果が期待と異なる場合があるため、Kindの設定とタイムゾーンの理解が重要です。

日付跨ぎの扱い

日付をまたぐ処理では、DateTimeDateプロパティやTimeSpanを活用して正確に判定する必要があります。

例えば、勤務時間の集計やログのロールオーバー処理で、日付跨ぎを正しく扱わないと誤った集計結果になることがあります。

DateTime start = new DateTime(2024, 6, 15, 22, 0, 0);
DateTime end = new DateTime(2024, 6, 16, 6, 0, 0);
TimeSpan duration = end - start; // 8時間

このように、終了日時が開始日時の翌日にまたがる場合でも、DateTimeの差分計算は正しく動作します。

ただし、日付だけを比較して処理を分ける場合は、Dateプロパティを使って日付部分のみを比較することがポイントです。

AddMonths の端数日問題

DateTime.AddMonths(int months)は、月数を加算しますが、元の日付が月末付近の場合、加算後の月に同じ日付が存在しないと自動的に月末日に調整されます。

これが「端数日問題」と呼ばれ、意図しない日付になることがあります。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2024, 1, 31);
        DateTime newDt = dt.AddMonths(1);
        Console.WriteLine($"元の日付: {dt:yyyy/MM/dd}");
        Console.WriteLine($"1ヶ月加算後: {newDt:yyyy/MM/dd}");
    }
}
元の日付: 2024/01/31
1ヶ月加算後: 2024/02/29

1月31日に1ヶ月加算すると、2月29日(うるう年の場合)に調整されます。

2月に29日がない年は28日になります。

この挙動は仕様なので、月末付近の日付を扱う場合は注意が必要です。

範囲外例外とオーバーフロー

DateTimeの有効範囲は、0001年1月1日0時0分0秒から9999年12月31日23時59分59秒9999999までです。

加算や減算でこの範囲を超えるとArgumentOutOfRangeExceptionが発生します。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = DateTime.MaxValue;
        try
        {
            DateTime newDt = dt.AddDays(1);
        }
        catch (ArgumentOutOfRangeException ex)
        {
            Console.WriteLine("範囲外例外が発生しました: " + ex.Message);
        }
    }
}
範囲外例外が発生しました: The added or subtracted value results in an un-representable DateTime. (Parameter 'value')

同様に、TimeSpanの加算や乗算でオーバーフローする場合も例外が発生します。

計算前に範囲チェックを行うか、例外処理を適切に実装して安全に扱うことが重要です。

パフォーマンス最適化

ValueType のコピーコスト

DateTimeTimeSpanは構造体ValueTypeであり、値型として扱われます。

値型は変数間で代入やメソッド呼び出し時にコピーが発生します。

コピーは参照型の参照渡しに比べてコストが高くなる場合があるため、パフォーマンスに影響を与えることがあります。

特に大量の日時データを頻繁に操作する場合や、ループ内でDateTimeTimeSpanを多用する場合は、コピー回数を減らす工夫が重要です。

例えば、メソッドの引数にrefinキーワードを使って参照渡しにすることで、コピーコストを削減できます。

using System;
class Program
{
    static void IncrementTicks(in DateTime dt, out DateTime result)
    {
        // inパラメータでコピーを抑制
        result = dt.AddTicks(1);
    }
    static void Main()
    {
        DateTime now = DateTime.Now;
        IncrementTicks(in now, out DateTime newDt);
        Console.WriteLine($"元の日時: {now}");
        Console.WriteLine($"1ティック加算後: {newDt}");
    }
}

このようにinを使うと、読み取り専用の参照渡しとなり、コピーを避けつつ安全に値を扱えます。

ticks を直接扱う高速化

DateTimeTimeSpanの内部表現はTicks(100ナノ秒単位の整数)です。

日時の加算や比較を高速化したい場合、Ticksを直接操作する方法があります。

例えば、DateTime.Ticksを取得して整数演算を行い、結果をnew DateTime(ticks)で再生成することで、メソッド呼び出しのオーバーヘッドを減らせます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = DateTime.Now;
        long ticks = dt.Ticks;
        // 1秒は10,000,000ティック
        long newTicks = ticks + 10_000_000;
        DateTime newDt = new DateTime(newTicks, dt.Kind);
        Console.WriteLine($"元の日時: {dt}");
        Console.WriteLine($"1秒加算後: {newDt}");
    }
}
元の日時: 2025/05/07 10:45:16
1秒加算後: 2025/05/07 10:45:17

この方法は大量の日時計算を行う際に有効ですが、Ticksの範囲外にならないよう注意が必要です。

DateTimeOffset の検討

DateTimeはタイムゾーン情報を持たず、KindプロパティでローカルかUTCかを区別するのみです。

そのため、タイムゾーンを跨ぐ日時処理や夏時間の考慮が必要な場合は、DateTimeOffsetの利用を検討してください。

DateTimeOffsetは日時とオフセット(UTCとの差)を一緒に保持し、タイムゾーンを明示的に扱えます。

これにより、日時の比較や変換がより正確かつ安全になります。

using System;
class Program
{
    static void Main()
    {
        DateTimeOffset dto = new DateTimeOffset(2024, 6, 15, 12, 0, 0, TimeSpan.FromHours(9)); // JST (UTC+9)
        DateTimeOffset utcDto = dto.ToUniversalTime();
        Console.WriteLine($"ローカル日時: {dto}");
        Console.WriteLine($"UTC日時: {utcDto}");
    }
}
ローカル日時: 2024/06/15 12:00:00 +09:00
UTC日時: 2024/06/15 3:00:00 +00:00

DateTimeOffsetDateTimeよりも扱いが複雑ですが、タイムゾーンを正確に管理したいシステムではパフォーマンスと正確性の両面で優れています。

パフォーマンス最適化の観点からも、タイムゾーン処理の誤りによるバグや再計算コストを減らせるため、検討する価値があります。

コードの可読性向上策

拡張メソッドで流暢なAPI

拡張メソッドを活用すると、既存のDateTimeTimeSpan型に対して新しいメソッドを追加でき、コードの可読性や表現力を高められます。

特に時間の単位変換や範囲生成など、よく使う処理をラップして流暢なAPIを作ると便利です。

TimeSpan.FromMinutes のラッパー

TimeSpan.FromMinutes(double)は分数からTimeSpanを生成しますが、より直感的に使えるように拡張メソッドでラップするとコードが読みやすくなります。

例えば、整数の分数を扱う場合にMinutes()メソッドを追加する例です。

using System;
static class TimeSpanExtensions
{
    public static TimeSpan Minutes(this int minutes)
    {
        return TimeSpan.FromMinutes(minutes);
    }
}
class Program
{
    static void Main()
    {
        TimeSpan ts = 15.Minutes();
        Console.WriteLine($"15分のTimeSpan: {ts}");
    }
}
15分のTimeSpan: 00:15:00

このように、15.Minutes()と書くことで、TimeSpan.FromMinutes(15)よりも自然な表現になり、コードの意図が明確になります。

DateTime.Range 生成

日時の範囲を表すRangeを生成する拡張メソッドを作ると、開始日時と終了日時のペアを簡単に扱えます。

以下は、DateTimeに対してRangeToメソッドを追加し、範囲を表すタプルを返す例です。

using System;
static class DateTimeExtensions
{
    public static (DateTime Start, DateTime End) RangeTo(this DateTime start, DateTime end)
    {
        if (end < start)
            throw new ArgumentException("終了日時は開始日時より後でなければなりません。");
        return (start, end);
    }
}
class Program
{
    static void Main()
    {
        DateTime start = new DateTime(2024, 6, 15, 9, 0, 0);
        DateTime end = new DateTime(2024, 6, 15, 17, 0, 0);
        var range = start.RangeTo(end);
        Console.WriteLine($"開始: {range.Start}");
        Console.WriteLine($"終了: {range.End}");
    }
}
開始: 2024/06/15 9:00:00
終了: 2024/06/15 17:00:00

このように範囲を表すメソッドを用意すると、日時の区間を扱う処理がシンプルかつ明確になります。

静的 using とグローバルインポート

C# 6.0以降では、using static構文を使って静的メソッドや定数を直接呼び出せるようになりました。

これにより、TimeSpan.FromMinutesDateTime.Nowなどの呼び出しを短縮し、コードの可読性を向上させられます。

using System;
using static System.TimeSpan;
using static System.DateTime;
class Program
{
    static void Main()
    {
        TimeSpan ts = FromMinutes(30);
        DateTime now = Now;
        Console.WriteLine($"現在時刻: {now}");
        Console.WriteLine($"30分のTimeSpan: {ts}");
    }
}
現在時刻: 2024/06/15 12:00:00
30分のTimeSpan: 00:30:00

さらに、C# 10以降ではグローバルusingディレクティブを使い、プロジェクト全体で共通の名前空間や静的クラスをインポートできます。

これにより、ファイルごとにusingを書く手間を省き、コードをすっきりさせられます。

// GlobalUsings.cs
global using static System.TimeSpan;
global using static System.DateTime;

この設定を行うと、プロジェクト内のすべてのファイルでFromMinutesNowを直接使えるようになります。

可読性だけでなく、開発効率も向上します。

テストとデバッグ

テスト時刻の固定化

日時を扱うコードのテストでは、実行時の現在日時が変動するため、テスト結果が不安定になりやすいです。

これを防ぐために、テスト時には日時を固定化して一定の値を返す仕組みを導入すると便利です。

こうした仕組みを作ることで、日時依存のロジックを安定して検証できます。

IClock インターフェース

IClockインターフェースを定義し、現在日時を取得するメソッドを抽象化します。

実装クラスを切り替えることで、実際の現在日時を返す本番用と、固定日時を返すテスト用を簡単に切り替えられます。

using System;
public interface IClock
{
    DateTime Now { get; }
}
public class SystemClock : IClock
{
    public DateTime Now => DateTime.Now;
}
public class FixedClock : IClock
{
    private readonly DateTime _fixedNow;
    public FixedClock(DateTime fixedNow)
    {
        _fixedNow = fixedNow;
    }
    public DateTime Now => _fixedNow;
}
class Program
{
    static void Main()
    {
        // 本番環境ではSystemClockを使用
        IClock clock = new SystemClock();
        Console.WriteLine($"本番環境の現在時刻: {clock.Now}");
        // テスト環境では固定日時を使用
        IClock testClock = new FixedClock(new DateTime(2024, 6, 15, 12, 0, 0));
        Console.WriteLine($"テスト環境の固定時刻: {testClock.Now}");
    }
}
本番環境の現在時刻: 2024/06/15 12:00:00
テスト環境の固定時刻: 2024/06/15 12:00:00

このようにIClockを使うことで、日時に依存する処理をテストしやすくなり、テストの再現性が向上します。

ログ出力のフォーマット

日時を含むログ出力では、フォーマットを統一することが重要です。

フォーマットがバラバラだとログ解析やトラブルシューティングが難しくなります。

ISO 8601形式(例:yyyy-MM-ddTHH:mm:ss.fffZ)を使うのが一般的で、UTC日時で記録するとタイムゾーンの混乱を避けられます。

using System;
class Program
{
    static void Main()
    {
        DateTime now = DateTime.UtcNow;
        string logTimestamp = now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
        Console.WriteLine($"ログ出力日時: {logTimestamp}");
    }
}
ログ出力日時: 2024-06-15T03:00:00.123Z

このフォーマットは機械可読性が高く、ログのソートやフィルタリングにも適しています。

ログ出力時は必ず日時のフォーマットを統一しましょう。

日時のシリアライズ検証

日時をJSONやXMLなどでシリアライズ・デシリアライズする際、フォーマットやタイムゾーンの扱いに注意が必要です。

特にDateTimeKindプロパティがUnspecifiedの場合、タイムゾーン情報が失われて誤解を招くことがあります。

.NETのSystem.Text.JsonNewtonsoft.Jsonでは、日時のシリアライズ時にISO 8601形式が使われますが、UTCかローカルかを明示的に扱うことが推奨されます。

using System;
using System.Text.Json;
class Program
{
    public class Event
    {
        public string Name { get; set; }
        public DateTime EventTime { get; set; }
    }
    static void Main()
    {
        var evt = new Event
        {
            Name = "Sample Event",
            EventTime = DateTime.SpecifyKind(new DateTime(2024, 6, 15, 12, 0, 0), DateTimeKind.Utc)
        };
        string json = JsonSerializer.Serialize(evt);
        Console.WriteLine($"シリアライズ結果: {json}");
        var deserialized = JsonSerializer.Deserialize<Event>(json);
        Console.WriteLine($"デシリアライズ結果: {deserialized.EventTime} (Kind: {deserialized.EventTime.Kind})");
    }
}
シリアライズ結果: {"Name":"Sample Event","EventTime":"2024-06-15T12:00:00Z"}
デシリアライズ結果: 2024/06/15 12:00:00 (Kind: Utc)

このように、日時のKindを明示的に指定し、UTCでシリアライズすることで、データの一貫性を保てます。

シリアライズ・デシリアライズ時の日時フォーマットとタイムゾーンの扱いは必ず検証してください。

関連ライブラリとツール

NodaTime の概要

NodaTimeは、.NET向けの高機能な日時処理ライブラリで、標準のDateTimeDateTimeOffsetの問題点を解消し、より正確で柔軟な日時操作を提供します。

タイムゾーンやカレンダーシステムの扱いが強化されており、夏時間の切り替えや複雑な日時計算も安全に行えます。

主な特徴は以下の通りです。

  • 不変(イミュータブル)な日時型を提供し、スレッドセーフ
  • 明確に区別されたローカル日時、UTC日時、オフセット付き日時を扱う型がある
  • IANAタイムゾーンデータベースを利用し、正確なタイムゾーン変換が可能
  • カレンダーシステムの切り替えが可能(グレゴリオ暦以外も対応)
  • 期間や間隔を表すPeriodDuration型を提供

簡単な使用例:

using System;
using NodaTime;
class Program
{
    static void Main()
    {
        var now = SystemClock.Instance.GetCurrentInstant();
        var tz = DateTimeZoneProviders.Tzdb["Asia/Tokyo"];
        var localTime = now.InZone(tz).LocalDateTime;
        Console.WriteLine($"現在のUTC時刻: {now}");
        Console.WriteLine($"東京の現地時刻: {localTime}");
    }
}
現在のUTC時刻: 2024-06-15T03:00:00Z
東京の現地時刻: 2024-06-15T12:00:00

NodaTimeは日時処理の正確性が求められるシステムや、複雑なタイムゾーン対応が必要な場合に特に有効です。

Humanizer で自然言語出力

Humanizerは、数値や日時、列挙型などを人間に読みやすい自然言語形式に変換するライブラリです。

日時に関しては、TimeSpanDateTimeの差分を「3時間前」「2日前」「1ヶ月後」などの表現に変換できます。

using System;
using Humanizer;
class Program
{
    static void Main()
    {
        TimeSpan ts = TimeSpan.FromHours(3);
        Console.WriteLine(ts.Humanize()); // "3 hours"
        DateTime past = DateTime.Now.AddDays(-2);
        Console.WriteLine(past.Humanize()); // "2 days ago"
        DateTime future = DateTime.Now.AddMonths(1);
        Console.WriteLine(future.Humanize()); // "in a month"
    }
}
3 hours
2 days ago
in a month

Humanizerを使うと、UI表示やログメッセージで日時を自然な言葉で表現でき、ユーザー体験が向上します。

BenchmarkDotNet で性能計測

BenchmarkDotNetは、.NETアプリケーションのコード性能を正確に計測するためのベンチマークライブラリです。

日時処理のパフォーマンス比較や最適化効果の検証に役立ちます。

使い方は簡単で、ベンチマーク対象のメソッドに[Benchmark]属性を付けて実行するだけです。

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
public class DateTimeBenchmark
{
    private DateTime dt = DateTime.Now;
    [Benchmark]
    public DateTime AddDaysMethod() => dt.AddDays(1);
    [Benchmark]
    public DateTime AddTicksMethod() => dt.AddTicks(TimeSpan.TicksPerDay);
}
class Program
{
    static void Main()
    {
        var summary = BenchmarkRunner.Run<DateTimeBenchmark>();
    }
}

実行すると、各メソッドの実行時間やメモリ使用量が詳細にレポートされます。

これにより、どの日時加算方法が高速か、どの処理がボトルネックかを科学的に判断できます。

BenchmarkDotNetはパフォーマンスチューニングの必須ツールとして広く使われています。

まとめ

この記事では、C#のDateTimeTimeSpanを使った日時の加算・減算方法から、実用的なシナリオや注意点、パフォーマンス最適化、可読性向上策まで幅広く解説しました。

基本的なメソッドの使い方だけでなく、夏時間やタイムゾーンの扱い、テスト時の日時固定化、関連ライブラリの活用法も理解できます。

これにより、日時処理の正確性と効率性を高め、堅牢でメンテナブルなコードを書くための知識が身につきます。

関連記事

Back to top button
目次へ