【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を使うことが多いです。
DateTime
のToLocalTime()
や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
を作成するには、Parse
とTryParse
メソッドを使います。
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
日時の文字列形式が決まっている場合は、ParseExact
やTryParseExact
を使うと、指定したフォーマットに厳密に従って変換できます。
これにより、曖昧な解釈を防げます。
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秒のTimeSpan
のTotalHours
は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
系は、時間の合計を単位換算して計算したい場合に便利です。
Days
やHours
はあくまで「部分的な値」であるのに対し、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系メソッドに負の値を渡す
DateTime
のAdd
系メソッドは、加算だけでなく負の値を渡すことで減算にも使えます。
例えば、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)の活用
DateTime
のSubtract(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) の違い
DateTime
のSubtract(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
Add
とSubtract
は演算子と同じ結果を返しますが、メソッド形式なのでコードの可読性や柔軟性を高める場面で役立ちます。
乗算と除算によるスケール
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
このように、Multiply
とDivide
の拡張メソッドを用意することで、TimeSpan
のスケール操作が簡単に行えます。
標準APIにない機能を補う形で活用してください。
DateTimeとTimeSpanの組み合わせテクニック
予定日時の計算シナリオ
翌営業日の取得
ビジネスシーンでは、翌営業日を計算することがよくあります。
土日や祝日を除外して、次の営業日を求めるにはDateTime
とTimeSpan
を組み合わせて処理します。
ここでは土日を休日と仮定し、翌営業日を取得する例を示します。
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日後に調整しています。
祝日を考慮する場合は、祝日リストを用意して判定を追加すると良いでしょう。
有効期限の判定シナリオ
商品の有効期限やライセンスの期限判定には、DateTime
とTimeSpan
の組み合わせが役立ちます。
例えば、購入日から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
プロパティを使うと、時間単位の合計も簡単に取得できます。
実用サンプル集
シフト表自動生成
従業員のシフト表を自動生成する際、DateTime
とTimeSpan
を活用して勤務開始時刻や終了時刻を計算します。
以下は、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時間以内に処理を完了させる必要があるタイマー処理では、DateTime
とTimeSpan
を使って残り時間を計算し、制限時間を超えたかどうかを判定します。
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時間の制限を超えたかどうかを判定しています。
データのロールオーバー処理
日付をまたぐデータ処理(ロールオーバー)では、DateTime
とTimeSpan
を使って処理の切り替えタイミングを判定します。
例えば、深夜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 プロパティの罠
DateTime
のKind
プロパティは、日時がローカル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
の設定とタイムゾーンの理解が重要です。
日付跨ぎの扱い
日付をまたぐ処理では、DateTime
のDate
プロパティや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 のコピーコスト
DateTime
やTimeSpan
は構造体ValueType
であり、値型として扱われます。
値型は変数間で代入やメソッド呼び出し時にコピーが発生します。
コピーは参照型の参照渡しに比べてコストが高くなる場合があるため、パフォーマンスに影響を与えることがあります。
特に大量の日時データを頻繁に操作する場合や、ループ内でDateTime
やTimeSpan
を多用する場合は、コピー回数を減らす工夫が重要です。
例えば、メソッドの引数にref
やin
キーワードを使って参照渡しにすることで、コピーコストを削減できます。
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 を直接扱う高速化
DateTime
やTimeSpan
の内部表現は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
DateTimeOffset
はDateTime
よりも扱いが複雑ですが、タイムゾーンを正確に管理したいシステムではパフォーマンスと正確性の両面で優れています。
パフォーマンス最適化の観点からも、タイムゾーン処理の誤りによるバグや再計算コストを減らせるため、検討する価値があります。
コードの可読性向上策
拡張メソッドで流暢なAPI
拡張メソッドを活用すると、既存のDateTime
やTimeSpan
型に対して新しいメソッドを追加でき、コードの可読性や表現力を高められます。
特に時間の単位変換や範囲生成など、よく使う処理をラップして流暢な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.FromMinutes
やDateTime.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;
この設定を行うと、プロジェクト内のすべてのファイルでFromMinutes
やNow
を直接使えるようになります。
可読性だけでなく、開発効率も向上します。
テストとデバッグ
テスト時刻の固定化
日時を扱うコードのテストでは、実行時の現在日時が変動するため、テスト結果が不安定になりやすいです。
これを防ぐために、テスト時には日時を固定化して一定の値を返す仕組みを導入すると便利です。
こうした仕組みを作ることで、日時依存のロジックを安定して検証できます。
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などでシリアライズ・デシリアライズする際、フォーマットやタイムゾーンの扱いに注意が必要です。
特にDateTime
のKind
プロパティがUnspecified
の場合、タイムゾーン情報が失われて誤解を招くことがあります。
.NETのSystem.Text.Json
やNewtonsoft.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向けの高機能な日時処理ライブラリで、標準のDateTime
やDateTimeOffset
の問題点を解消し、より正確で柔軟な日時操作を提供します。
タイムゾーンやカレンダーシステムの扱いが強化されており、夏時間の切り替えや複雑な日時計算も安全に行えます。
主な特徴は以下の通りです。
- 不変(イミュータブル)な日時型を提供し、スレッドセーフ
- 明確に区別されたローカル日時、UTC日時、オフセット付き日時を扱う型がある
- IANAタイムゾーンデータベースを利用し、正確なタイムゾーン変換が可能
- カレンダーシステムの切り替えが可能(グレゴリオ暦以外も対応)
- 期間や間隔を表す
Period
やDuration
型を提供
簡単な使用例:
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
は、数値や日時、列挙型などを人間に読みやすい自然言語形式に変換するライブラリです。
日時に関しては、TimeSpan
やDateTime
の差分を「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#のDateTime
とTimeSpan
を使った日時の加算・減算方法から、実用的なシナリオや注意点、パフォーマンス最適化、可読性向上策まで幅広く解説しました。
基本的なメソッドの使い方だけでなく、夏時間やタイムゾーンの扱い、テスト時の日時固定化、関連ライブラリの活用法も理解できます。
これにより、日時処理の正確性と効率性を高め、堅牢でメンテナブルなコードを書くための知識が身につきます。