C# の DateTime は日付・時刻を扱う最も基本的な型ですが、「タイムゾーンの扱い方がわからない」「Parse が環境によって動作が変わる」「DateOnly と DateTime の使い分けがわからない」という疑問を持つ方も多いです。
本記事では DateTime の基礎から、DateTimeOffset・DateOnly・TimeOnly といったモダンな型、タイムゾーン変換、書式文字列一覧、年齢計算などの実践例まで体系的に解説します。
日付・時刻を扱う型の全体マップ
C# には用途に応じた複数の日付・時刻型があります。最初に全体像を把握してから使う型を選びましょう。
| 型 | 意味 | 主な用途 |
|---|---|---|
DateTime |
ローカル or UTC の日付+時刻 | 最も汎用的。単一タイムゾーン内で使う場合に適切 |
DateTimeOffset |
UTC オフセット付きの日付+時刻 | 異なるタイムゾーン間で日時を比較・保存するなら必須 |
DateOnly |
日付のみ(時刻なし) | .NET 6+。誕生日・カレンダー日付など時刻が不要な場面 |
TimeOnly |
時刻のみ(日付なし) | .NET 6+。営業時間・アラーム時刻など日付が不要な場面 |
TimeSpan |
時間の長さ(期間) | 2つの DateTime の差・タイマー経過時間など |
DateTimeOffset を選ぶのが安全です。DateTime は Kind プロパティ(Local/Utc/Unspecified)があいまいになりやすく、タイムゾーンのバグの温床になります。DateTime の生成
// ─── 現在日時 ───────────────────────────────────── DateTime local = DateTime.Now; // ローカル日時(Kind=Local) DateTime utc = DateTime.UtcNow; // UTC 日時(Kind=Utc) DateTime today = DateTime.Today; // 今日の日付(時刻は 0:00:00、Kind=Local) // ─── 固定値 ────────────────────────────────────── DateTime min = DateTime.MinValue; // 0001/01/01 00:00:00 DateTime max = DateTime.MaxValue; // 9999/12/31 23:59:59 // ─── コンストラクタ ──────────────────────────────── DateTime d1 = new DateTime(2025, 8, 28); // 年月日 DateTime d2 = new DateTime(2025, 8, 28, 14, 30, 0); // 年月日時分秒 DateTime d3 = new DateTime(2025, 8, 28, 14, 30, 0, 500); // ミリ秒付き // ─── Kind を指定して生成 ────────────────────────── DateTime local2 = new DateTime(2025, 8, 28, 9, 0, 0, DateTimeKind.Local); DateTime utc2 = new DateTime(2025, 8, 28, 0, 0, 0, DateTimeKind.Utc); // ─── Ticks から生成(100ns 単位)───────────────── long ticks = DateTime.Now.Ticks; DateTime fromTicks = new DateTime(ticks);
Kind プロパティ:タイムゾーンの落とし穴
DateTime には Kind プロパティがあり、3 種類の値を持ちます。この Kind の扱いを誤ると、タイムゾーン変換時に予期しない挙動になります。
| Kind 値 | 生成方法 | 意味 |
|---|---|---|
DateTimeKind.Local |
DateTime.Now・DateTime.Today |
ローカルタイムゾーンの時刻 |
DateTimeKind.Utc |
DateTime.UtcNow |
UTC の時刻 |
DateTimeKind.Unspecified |
コンストラクタのデフォルト | タイムゾーン不明(要注意) |
DateTime now = DateTime.Now; Console.WriteLine(now.Kind); // Local DateTime utc = DateTime.UtcNow; Console.WriteLine(utc.Kind); // Utc // コンストラクタのデフォルトは Unspecified DateTime d = new DateTime(2025, 8, 28); Console.WriteLine(d.Kind); // Unspecified // ─── Kind を指定して新しい DateTime を作る ──────── DateTime asUtc = DateTime.SpecifyKind(d, DateTimeKind.Utc); DateTime asLocal = DateTime.SpecifyKind(d, DateTimeKind.Local); // ─── UTC ↔ Local の変換 ───────────────────────── DateTime localTime = utc.ToLocalTime(); // UTC → ローカル DateTime utcTime = now.ToUniversalTime(); // ローカル → UTC
ToLocalTime()・ToUniversalTime() は Kind が Unspecified のとき「Local として扱う」などの暗黙的な仮定が入り、想定外の変換になることがあります。複数のタイムゾーンが絡む場合は DateTimeOffset を使いましょう。主要プロパティ
DateTime dt = new DateTime(2025, 8, 28, 14, 30, 45, 123); // ─── 日付・時刻の各要素 ────────────────────────── Console.WriteLine(dt.Year); // 2025 Console.WriteLine(dt.Month); // 8 Console.WriteLine(dt.Day); // 28 Console.WriteLine(dt.Hour); // 14 Console.WriteLine(dt.Minute); // 30 Console.WriteLine(dt.Second); // 45 Console.WriteLine(dt.Millisecond); // 123 // ─── その他のプロパティ ────────────────────────── Console.WriteLine(dt.DayOfWeek); // Thursday(曜日) Console.WriteLine(dt.DayOfYear); // 240(その年の何日目か) Console.WriteLine(dt.Ticks); // 638,596,326,451,230,000(100ns 単位) // ─── 日付部分・時刻部分のみ取得 ───────────────── DateTime dateOnly = dt.Date; // 2025/08/28 00:00:00(時刻を切り捨て) TimeSpan timeOnly = dt.TimeOfDay; // 14:30:45.123(時刻部分を TimeSpan で取得)
| プロパティ | 返す値の例 | 用途 |
|---|---|---|
DayOfWeek |
DayOfWeek.Monday ~ Sunday |
曜日を enum で返す |
DayOfYear |
1 ~ 366 | 元日は 1、大晦日は 365 or 366 |
Ticks |
0001/01/01 からの 100ns 単位カウント | 高精度の時刻比較や記録に使える |
Date |
時刻部分を 0:00:00 に切り捨てた DateTime |
日付のみの比較に便利 |
TimeOfDay |
TimeSpan で時刻部分を返す |
時刻のみの比較に便利 |
日付・時刻の計算
Add 系メソッドで日付を加算・減算する
DateTime now = new DateTime(2025, 1, 31);
// ─── 加算(正の値)・減算(負の値)────────────────
Console.WriteLine(now.AddYears(1)); // 2026/01/31
Console.WriteLine(now.AddMonths(1)); // 2025/02/28 ← 2/31 は存在しないので末日に丸められる!
Console.WriteLine(now.AddDays(7)); // 2025/02/07
Console.WriteLine(now.AddHours(-2)); // 2025/01/30 22:00:00
Console.WriteLine(now.AddMinutes(90)); // 2025/01/31 01:30:00
Console.WriteLine(now.AddSeconds(-1)); // 2025/01/30 23:59:59
Console.WriteLine(now.AddMilliseconds(500));
Console.WriteLine(now.AddTicks(10_000_000)); // 1秒 = 10,000,000 ticks
// ─── AddTicks は精度が必要な場面で使う ────────────
DateTime start = DateTime.UtcNow;
// ... 処理 ...
DateTime end = DateTime.UtcNow;
TimeSpan elapsed = end - start; // 差分は TimeSpan で取れる
Console.WriteLine($"経過: {elapsed.TotalMilliseconds} ms");
AddMonths(1) は「1ヶ月後」を計算しますが、月末日の扱いに注意が必要です。1月31日の1ヶ月後は2月31日ではなく、2月の最終日(2月28日 or 29日)に自動的に丸められます。TimeSpan:時間の長さを表す型
// ─── 2つの DateTime の差(TimeSpan を返す)────────
DateTime start = new DateTime(2025, 1, 1);
DateTime end = new DateTime(2025, 12, 31);
TimeSpan diff = end - start;
Console.WriteLine(diff.Days); // 364
Console.WriteLine(diff.TotalHours); // 8736.0
Console.WriteLine(diff.TotalMinutes); // 524160.0
// ─── TimeSpan の生成 ─────────────────────────────
TimeSpan ts1 = new TimeSpan(2, 30, 0); // 2時間30分0秒
TimeSpan ts2 = TimeSpan.FromHours(1.5); // 1.5時間
TimeSpan ts3 = TimeSpan.FromDays(7); // 7日間
// ─── DateTime と TimeSpan の演算 ─────────────────
DateTime deadline = DateTime.Now.Add(TimeSpan.FromDays(30)); // 30日後
DateTime yesterday = DateTime.Today - TimeSpan.FromDays(1); // 昨日
// ─── 残り時間の計算 ──────────────────────────────
TimeSpan remaining = deadline - DateTime.Now;
Console.WriteLine($"残り {remaining.Days} 日 {remaining.Hours} 時間");
DateTime の比較
DateTime d1 = new DateTime(2025, 8, 28); DateTime d2 = new DateTime(2025, 9, 1); // ─── 比較演算子(最も直感的)───────────────────── bool isBefore = d1 < d2; // true bool isAfter = d1 > d2; // false bool isEqual = d1 == d2; // false bool isSameOrBefore = d1 <= d2; // true // ─── CompareTo(ソートや三値比較に使う)────────── int result = d1.CompareTo(d2); // -1(d1 < d2) // 0 なら同じ、1 なら d1 > d2 // ─── DateTime.Compare 静的メソッド ──────────────── int cmp = DateTime.Compare(d1, d2); // -1 // ─── 同じ日付かどうか(時刻を無視して比較)──────── bool sameDay = d1.Date == d2.Date; // ─── 期間内かどうかの判定 ──────────────────────── DateTime today = DateTime.Today; DateTime periodStart = new DateTime(2025, 4, 1); DateTime periodEnd = new DateTime(2025, 6, 30); bool isInPeriod = today >= periodStart && today <= periodEnd; Console.WriteLine(isInPeriod ? "期間内" : "期間外");
DateTime は構造体(値型)のため、== 演算子で内容が同じかどうかを比較できます。ただし Kind(Local/Utc)が異なる場合でも比較できてしまう(内部の Ticks 値が等しければ true)ため、タイムゾーンをまたいだ比較には DateTimeOffset を使うべきです。書式変換:ToString・Parse・TryParse
ToString でフォーマットを指定する
DateTime dt = new DateTime(2025, 8, 28, 14, 30, 45);
// ─── 標準書式(定義済みパターン)─────────────────
Console.WriteLine(dt.ToString("d")); // 2025/08/28 (短い日付)
Console.WriteLine(dt.ToString("D")); // 2025年8月28日 (長い日付)
Console.WriteLine(dt.ToString("t")); // 14:30 (短い時刻)
Console.WriteLine(dt.ToString("T")); // 14:30:45 (長い時刻)
Console.WriteLine(dt.ToString("f")); // 2025年8月28日 14:30
Console.WriteLine(dt.ToString("F")); // 2025年8月28日 14:30:45
Console.WriteLine(dt.ToString("g")); // 2025/08/28 14:30
Console.WriteLine(dt.ToString("G")); // 2025/08/28 14:30:45
Console.WriteLine(dt.ToString("s")); // 2025-08-28T14:30:45 (ISO 8601)
Console.WriteLine(dt.ToString("o")); // 2025-08-28T14:30:45.0000000 (ラウンドトリップ形式)
Console.WriteLine(dt.ToString("R")); // Thu, 28 Aug 2025 14:30:45 GMT (HTTP 用)
// ─── カスタム書式 ─────────────────────────────────
Console.WriteLine(dt.ToString("yyyy/MM/dd")); // 2025/08/28
Console.WriteLine(dt.ToString("yyyy年MM月dd日")); // 2025年08月28日
Console.WriteLine(dt.ToString("HH:mm:ss")); // 14:30:45
Console.WriteLine(dt.ToString("yyyy-MM-dd HH:mm:ss")); // 2025-08-28 14:30:45
Console.WriteLine(dt.ToString("M月d日(ddd)")); // 8月28日(木)
// ─── 文字列補間でも使える ─────────────────────────
Console.WriteLine($"{dt:yyyy/MM/dd}"); // 2025/08/28
Console.WriteLine($"{dt:HH時mm分ss秒}"); // 14時30分45秒
| 書式指定子 | 意味 | 例(2025/8/28 14:30:45) |
|---|---|---|
yyyy |
年(4桁) | 2025 |
yy |
年(2桁) | 25 |
MM |
月(2桁・ゼロ埋め) | 08 |
M |
月(ゼロ埋めなし) | 8 |
dd |
日(2桁・ゼロ埋め) | 28 |
d |
日(ゼロ埋めなし) | 28 |
HH |
時(24時間・2桁) | 14 |
hh |
時(12時間・2桁) | 02 |
mm |
分(2桁) | 30 |
ss |
秒(2桁) | 45 |
fff |
ミリ秒(3桁) | 000 |
ddd |
曜日(省略形) | 木 |
dddd |
曜日(フル) | 木曜日 |
tt |
午前/午後 | 午後 |
K |
タイムゾーン(Kind に応じて) | +09:00 or Z |
Parse・TryParse:文字列から DateTime へ
// ─── Parse: パース失敗すると例外 ─────────────────
DateTime d1 = DateTime.Parse("2025/08/28");
DateTime d2 = DateTime.Parse("2025-08-28T14:30:45");
// ─── TryParse: 失敗しても例外が出ない(安全)─────
string input = "2025/08/28";
if (DateTime.TryParse(input, out DateTime result))
Console.WriteLine($"パース成功: {result}");
else
Console.WriteLine("パース失敗");
// ─── ParseExact: 書式を厳密に指定 ────────────────
// ユーザー入力・外部ファイルの日付文字列を正確にパース
DateTime exact = DateTime.ParseExact(
"28-08-2025 14:30",
"dd-MM-yyyy HH:mm",
System.Globalization.CultureInfo.InvariantCulture
);
Console.WriteLine(exact); // 2025/08/28 14:30:00
// ─── TryParseExact: ParseExact の安全版 ──────────
string csvDate = "20250828";
bool ok = DateTime.TryParseExact(
csvDate,
"yyyyMMdd",
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None,
out DateTime parsed
);
if (ok) Console.WriteLine($"CSV日付: {parsed:yyyy/MM/dd}");
// ─── 複数の書式を一度に試す ───────────────────────
string[] formats = { "yyyy/MM/dd", "yyyy-MM-dd", "yyyyMMdd" };
bool ok2 = DateTime.TryParseExact(
"2025-08-28", formats,
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None,
out DateTime multi
);
DateTime.Parse("8/28/2025") は実行環境のカルチャ設定によって月と日が逆に解釈されることがあります(米国環境では Month/Day、日本環境では年/月/日)。外部入力のパースには必ず ParseExact か TryParseExact に CultureInfo.InvariantCulture を指定しましょう。静的ユーティリティメソッド
// ─── うるう年の判定 ────────────────────────────── Console.WriteLine(DateTime.IsLeapYear(2024)); // true Console.WriteLine(DateTime.IsLeapYear(2025)); // false // ─── 特定の月の日数を取得 ──────────────────────── Console.WriteLine(DateTime.DaysInMonth(2024, 2)); // 29(うるう年) Console.WriteLine(DateTime.DaysInMonth(2025, 2)); // 28 Console.WriteLine(DateTime.DaysInMonth(2025, 4)); // 30 // 月の最終日を取得する実用的なパターン DateTime lastDay = new DateTime(2025, 2, DateTime.DaysInMonth(2025, 2)); Console.WriteLine(lastDay); // 2025/02/28 // ─── Kind を変更して新しい DateTime を作る ──────── DateTime parsed = new DateTime(2025, 8, 28); // Kind=Unspecified DateTime asUtc = DateTime.SpecifyKind(parsed, DateTimeKind.Utc); Console.WriteLine(asUtc.Kind); // Utc
DateTimeOffset:タイムゾーンを正確に扱う
DateTimeOffset は DateTime に UTC オフセット(+09:00 など)を付加した型です。タイムゾーンが異なる日時を正確に比較・変換できます。
// ─── 現在日時(ローカルオフセット付き)──────────
DateTimeOffset now = DateTimeOffset.Now; // 2025-08-28T14:30:45+09:00
DateTimeOffset utc = DateTimeOffset.UtcNow; // 2025-08-28T05:30:45+00:00
Console.WriteLine(now.Offset); // +09:00:00(現在のUTCオフセット)
Console.WriteLine(now.UtcDateTime); // UTC に変換した DateTime を返す
// ─── 固定オフセットを指定して生成 ────────────────
var dto = new DateTimeOffset(2025, 8, 28, 14, 30, 0, TimeSpan.FromHours(9));
Console.WriteLine(dto); // 2025-08-28T14:30:00+09:00
// ─── DateTime から変換 ────────────────────────────
DateTime local = DateTime.Now;
DateTimeOffset fromLocal = new DateTimeOffset(local); // Kind=Local のオフセットを継承
// ─── タイムゾーンをまたいだ比較 ─────────────────
var tokyo = new DateTimeOffset(2025, 8, 28, 14, 0, 0, TimeSpan.FromHours(9));
var newYork = new DateTimeOffset(2025, 8, 28, 1, 0, 0, TimeSpan.FromHours(-4));
// UTC に正規化すると両者ともに 2025-08-28T05:00:00Z → 同じ瞬間として true
Console.WriteLine(tokyo == newYork); // true(UTC で一致)
// DateTimeOffset.Now と .UtcNow は取得タイミングが微妙にずれるため == は不確実
// 同じ値を変換して比較するのが確実
DateTimeOffset t = DateTimeOffset.Now;
Console.WriteLine(t == t.ToUniversalTime()); // true(同じ瞬間を UTC に変換)
// ─── ISO 8601 文字列の変換 ────────────────────────
DateTimeOffset parsed = DateTimeOffset.Parse("2025-08-28T14:30:00+09:00");
string iso8601 = parsed.ToString("o"); // 2025-08-28T14:30:00.0000000+09:00
DateTimeOffset を使い、ISO 8601 形式("o" 書式)で文字列化すると相互運用性が高まります。UTC で保存し表示時にローカル変換するのがベストプラクティスです。DateOnly・TimeOnly:.NET 6 以降の新型
.NET 6 で導入された DateOnly と TimeOnly は「日付だけ」「時刻だけ」を表す専用型です。DateTime に時刻 0:00:00 を付けて日付を表す必要がなくなります。
// ─── DateOnly: 日付のみ ──────────────────────────
DateOnly today = DateOnly.FromDateTime(DateTime.Today);
DateOnly birthday = new DateOnly(1990, 4, 15);
Console.WriteLine(today); // 2025-08-28
Console.WriteLine(birthday.Year); // 1990
Console.WriteLine(birthday.DayOfWeek); // Sunday
DateOnly nextMonth = today.AddMonths(1);
int span = today.DayNumber - birthday.DayNumber; // 日数差
// ─── TimeOnly: 時刻のみ ──────────────────────────
TimeOnly openTime = new TimeOnly(9, 0, 0); // 09:00:00
TimeOnly closeTime = new TimeOnly(18, 30, 0); // 18:30:00
TimeOnly now2 = TimeOnly.FromDateTime(DateTime.Now);
// 営業時間内かどうかの判定
bool isOpen = now2.IsBetween(openTime, closeTime);
Console.WriteLine(isOpen ? "営業中" : "営業時間外");
Console.WriteLine(openTime.ToString("HH:mm")); // 09:00
Console.WriteLine(closeTime.ToString("h:mm tt")); // 6:30 PM
// ─── DateOnly × TimeOnly → DateTime に変換 ──────
DateTime dt = today.ToDateTime(now2); // DateOnly + TimeOnly → DateTime
| ユースケース | 推奨型 | 理由 |
|---|---|---|
| 誕生日・期日・カレンダー | DateOnly |
時刻が不要な概念には DateTime の 0:00:00 より意味が明確 |
| 営業時間・アラーム時刻 | TimeOnly |
日付を持たない時刻の概念に最適 |
| ログの正確な記録・API | DateTimeOffset |
タイムゾーンを含む完全な日時 |
| 単一タイムゾーン内の処理 | DateTime |
従来どおり使える。ただし Kind に注意 |
TimeZoneInfo:タイムゾーン変換
// ─── タイムゾーンを指定して変換 ──────────────────
// Windows では "Tokyo Standard Time"、Linux/macOS では "Asia/Tokyo"
TimeZoneInfo jst = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
TimeZoneInfo pst = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
DateTime utcNow = DateTime.UtcNow;
// UTC → 東京時間
DateTime tokyoTime = TimeZoneInfo.ConvertTimeFromUtc(utcNow, jst);
Console.WriteLine($"東京: {tokyoTime:yyyy/MM/dd HH:mm:ss}");
// UTC → 太平洋標準時
DateTime pstTime = TimeZoneInfo.ConvertTimeFromUtc(utcNow, pst);
Console.WriteLine($"PST: {pstTime:yyyy/MM/dd HH:mm:ss}");
// ─── クロスプラットフォーム対応 ─────────────────
// Windows と Linux/macOS でタイムゾーン ID が異なる問題の解決策
// NuGet: TimeZoneConverter パッケージを使うと両環境で同じコードが書ける
// TimeZoneInfo jst2 = TZConvert.GetTimeZoneInfo("Asia/Tokyo"); // TimeZoneConverter
// ─── 夏時間を考慮した判定 ─────────────────────────
bool isDST = pst.IsDaylightSavingTime(pstTime);
Console.WriteLine($"夏時間適用中: {isDST}");
Console.WriteLine($"UTCオフセット: {pst.GetUtcOffset(pstTime)}");
// ─── 全タイムゾーン一覧 ──────────────────────────
foreach (var tz in TimeZoneInfo.GetSystemTimeZones())
Console.WriteLine($"{tz.Id}: {tz.DisplayName}");
実践例
年齢を計算する
static int CalculateAge(DateOnly birthDate, DateOnly today)
{
int age = today.Year - birthDate.Year;
// まだ誕生日が来ていない場合は 1 引く
if (today < birthDate.AddYears(age))
age--;
return age;
}
// 使い方
DateOnly birthday = new DateOnly(1990, 2, 28);
DateOnly today = DateOnly.FromDateTime(DateTime.Today);
Console.WriteLine($"年齢: {CalculateAge(birthday, today)} 歳");
// うるう年生まれ(2/29)への対応
// DateOnly.AddYears は 2/29 → 2/28 に丸めるため正しく動作する
DateOnly leapBirth = new DateOnly(2000, 2, 29);
Console.WriteLine(CalculateAge(leapBirth, today));
期限判定と残り日数
record TaskItem(string Name, DateTime Deadline);
static string GetDeadlineStatus(TaskItem task)
{
DateTime now = DateTime.Now;
TimeSpan remaining = task.Deadline - now;
if (remaining.TotalDays < 0)
return $"[期限超過] {task.Name} ({Math.Abs((int)remaining.TotalDays)} 日前に期限切れ)";
if (remaining.TotalDays < 1)
return $"[今日締切] {task.Name} (残り {(int)remaining.TotalHours} 時間)";
if (remaining.TotalDays < 7)
return $"[警告] {task.Name} (残り {(int)remaining.TotalDays} 日)";
return $"[正常] {task.Name} (期限: {task.Deadline:yyyy/MM/dd})";
}
// 使い方
var tasks = new[]
{
new TaskItem("設計書作成", DateTime.Today.AddDays(-1)), // 昨日が期限
new TaskItem("コードレビュー", DateTime.Today.AddHours(3)), // 今日
new TaskItem("テスト実施", DateTime.Today.AddDays(5)), // 5日後
new TaskItem("リリース", DateTime.Today.AddDays(30)), // 30日後
};
foreach (var task in tasks)
Console.WriteLine(GetDeadlineStatus(task));
月の営業日を列挙する
static IEnumerable<DateOnly> GetBusinessDays(int year, int month)
{
int daysInMonth = DateTime.DaysInMonth(year, month);
for (int day = 1; day <= daysInMonth; day++)
{
var date = new DateOnly(year, month, day);
if (date.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday)
yield return date;
}
}
// 使い方
var workdays = GetBusinessDays(2025, 8).ToList();
Console.WriteLine($"2025年8月の営業日数: {workdays.Count} 日");
Console.WriteLine($"最初の営業日: {workdays.First()}");
Console.WriteLine($"最後の営業日: {workdays.Last()}");
よくある落とし穴と注意点
DateTime.Now をテストで使うとテストが不安定になる
DateTime.Now を直接呼び出すコードはユニットテストで問題になります。テスト実行タイミングによって結果が変わるためです。現在時刻を取得する処理は Func<DateTime> clock や IClock インターフェースを引数で受け取るよう設計し、テスト時はモックに差し替えられるようにしましょう。
Kind の異なる DateTime を比較するとバグになる
// BAD: ToUniversalTime() で変換しても Kind が違うため Ticks が一致せず == は false DateTime someTime = new DateTime(2025, 8, 28, 14, 0, 0, DateTimeKind.Local); DateTime utcVersion = someTime.ToUniversalTime(); // Kind=Utc、例: 05:00 UTC bool wrongResult = someTime == utcVersion; // false(14:00 と 05:00 の Ticks が違う) // GOOD: DateTimeOffset は UTC に正規化してから比較するため正しく一致する DateTimeOffset dtoLocal = new DateTimeOffset(someTime); DateTimeOffset dtoUtc = dtoLocal.ToUniversalTime(); bool correct = dtoLocal == dtoUtc; // true(同じ瞬間として正しく比較)
AddMonths は月末日を自動丸める
1月31日の1ヶ月後を求める際に AddMonths(1) を使うと、自動的に2月の最終日(28日 or 29日)に丸められます。「毎月末日に繰り返す」処理を実装する場合は DateTime.DaysInMonth(year, month) で明示的に末日を求める必要があります。
Parse はカルチャに依存する
DateTime.Parse("04/08/2025") は日本語環境では「4月8日」ですが、英語(米国)環境では「8月4日」と解釈されます。外部から受け取った日付文字列は必ず ParseExact に CultureInfo.InvariantCulture を指定して書式を固定してください。
よくある質問
DateTime(UtcNow を使って UTC で統一)が十分です。複数のタイムゾーンをまたぐ可能性がある場合(グローバルサービス・データベース保存・API 連携)は DateTimeOffset を使うと安全です。新規プロジェクトでは最初から DateTimeOffset を選ぶのが無難です。DateTime.Now は実行マシンのローカルタイムゾーンの現在時刻(Kind=Local)を返します。DateTime.UtcNow は UTC 時刻(Kind=Utc)を返します。ログ・データベース保存には UTC を使うのが一般的です。UtcNow の方が Now より若干高速(ローカル変換が不要)という側面もあります。DateOnly と TimeOnly は .NET 6 以降のみで利用できます。.NET Framework では利用できません。.NET Framework では DateTime.Date で日付部分を取り出すか、new DateTime(year, month, day) を使ってください。DateTime 同士を引き算すると TimeSpan が返ります。(d2 - d1).Days で整数の日数差が取れます。DateOnly の場合は d2.DayNumber - d1.DayNumber で日数差を計算できます。時刻を無視して純粋に日数を数えたい場合は d2.Date - d1.Date として時刻部分を切り捨ててから引き算するか、DateOnly を使いましょう。TimeZoneInfo.FindSystemTimeZoneById で指定するタイムゾーン ID が Windows("Tokyo Standard Time")と Linux/macOS("Asia/Tokyo")で異なります。NuGet の TimeZoneConverter パッケージを使うと、IANA 形式("Asia/Tokyo")で統一して両プラットフォームで動作するコードが書けます。まとめ
| 内容 | ポイント |
|---|---|
| 型の選択 | 日付のみ → DateOnly、時刻のみ → TimeOnly、タイムゾーン跨ぎ → DateTimeOffset |
| Kind プロパティ | Unspecified は要注意。UTC で統一して Local/Utc を明示する |
| 日付計算 | AddX メソッドで加減算。月末日は DaysInMonth で確認 |
| 書式変換 | ToString("yyyy/MM/dd")、文字列補間 $"{dt:HH:mm}" |
| パース | 外部入力は TryParseExact + InvariantCulture で書式を固定 |
| タイムゾーン | TimeZoneInfo.ConvertTimeFromUtc で変換。クロスプラットフォームは TimeZoneConverter |
| テスト | DateTime.Now を直接使わず、時刻取得を DI で差し替え可能にする |
DateTime で生成した文字列を加工する場面では文字列操作完全ガイド、書式文字列を文字列補間と組み合わせる方法は文字列補間($”…”)の基本と活用例も参考にしてください。