日時

【C#】DateTimeを自在にフォーマットする方法まとめ—標準書式・カスタム指定子と実践テクニック

DateTime型はToStringに書式指定子を渡すだけで和暦、ISO8601、年‐月‐日 時:分:秒などを自在に整形できます。

標準指定子は短く、カスタム指定子は自由度が高いので場面に応じた表示が簡単です。

ログやUIの統一、並べ替えに強く、実務で即戦力になる機能です。

目次から探す
  1. 基本的なフォーマット指定の仕組み
  2. 標準書式指定子カタログ
  3. カスタム書式指定子の詳細
  4. カルチャとローカリゼーション
  5. 実務で役立つフォーマットパターン集
  6. DateTimeOffsetとタイムゾーン
  7. パフォーマンスとメモリ効率
  8. よくあるエラーとデバッグ方法
  9. .NET 6以降の新しい型
  10. カスタムIFormatProviderの活用
  11. 外部ライブラリによる拡張
  12. テストでフォーマットを保証する
  13. 参考用クイックチャート
  14. まとめ

基本的なフォーマット指定の仕組み

C#で日付や時刻を文字列に変換する際には、DateTimeDateTimeOffsetのインスタンスに対してフォーマット指定子を使うことが一般的です。

ここでは、これらの型の違いや、フォーマット指定子がどのように適用されるか、また文字列補間やstring.Formatとの使い分けについて詳しく解説します。

DateTimeとDateTimeOffsetの違い

DateTimeDateTimeOffsetはどちらも日時を表す型ですが、扱う情報に違いがあります。

  • DateTime

日付と時刻を表しますが、タイムゾーンの情報は持ちません。

Kindプロパティでローカル時間、UTC、または不明を示すことはできますが、実際のオフセット情報は含まれていません。

そのため、単純な日時の表現やローカル時間の処理に向いていますが、タイムゾーンをまたぐ日時の比較や保存には注意が必要です。

  • DateTimeOffset

日付と時刻に加えて、UTCからのオフセット(時差)を持ちます。

これにより、世界中の異なるタイムゾーンの日時を正確に表現できます。

例えば、2025-05-07 08:55:31 +09:00のように、時刻とそのオフセットがセットで管理されます。

タイムゾーンを意識した日時の処理や、ログのタイムスタンプ、分散システムでの日時管理に適しています。

サンプルコード:DateTimeとDateTimeOffsetの違いを確認する

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31, DateTimeKind.Local);
        DateTimeOffset dto = new DateTimeOffset(2025, 5, 7, 8, 55, 31, TimeSpan.FromHours(9));
        Console.WriteLine("DateTime: " + dt.ToString("o"));       // ラウンドトリップ形式
        Console.WriteLine("DateTime Kind: " + dt.Kind);
        Console.WriteLine("DateTimeOffset: " + dto.ToString("o")); // ラウンドトリップ形式
        Console.WriteLine("Offset: " + dto.Offset);
    }
}
DateTime: 2025-05-07T08:55:31.0000000+09:00
DateTime Kind: Local
DateTimeOffset: 2025-05-07T08:55:31.0000000+09:00
Offset: 09:00:00

この例では、DateTimeKindLocalであることと、DateTimeOffsetが明示的にオフセットを持っていることがわかります。

DateTimeToString("o")Kindに応じてオフセットを付加しますが、DateTimeOffsetは常にオフセットを含みます。

ToStringと書式指定子の関係

日時を文字列に変換する際、ToStringメソッドに書式指定子を渡すことで、表示形式を自由にコントロールできます。

書式指定子には大きく分けて「標準書式指定子」と「カスタム書式指定子」があります。

  • 標準書式指定子

事前に定義された形式を簡単に指定できる文字列(例:”d”, “D”, “f”, “F”など)です。

例:DateTime.Now.ToString("D")2025年5月7日水曜日

  • カスタム書式指定子

年、月、日、時、分、秒などの要素を細かく指定して組み合わせることができます。

例:DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")2025/05/07 08:55:31

標準書式指定子が呼び出される流れ

ToStringに標準書式指定子を渡すと、内部的にはDateTimeFormatInfoクラスがその指定子に対応するフォーマットパターンを取得し、適用します。

たとえば、”D”はロングデートパターン(例:yyyy年M月d日 dddd)に展開されます。

この展開は、現在のカルチャCultureInfo.CurrentCultureに依存しており、同じ指定子でも言語や地域設定によって表示が変わります。

サンプルコード:標準書式指定子の展開例

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        CultureInfo ci = new CultureInfo("ja-JP");
        Console.WriteLine(dt.ToString("D", ci)); // 長い日付形式
        Console.WriteLine(dt.ToString("d", ci)); // 短い日付形式
        Console.WriteLine(dt.ToString("F", ci)); // 長い日時形式
        Console.WriteLine(dt.ToString("f", ci)); // 短い日時形式
    }
}
2025年5月7日
2025/05/07
2025年5月7日 8:55:31
2025年5月7日 8:55

このように、標準書式指定子はカルチャに応じたパターンに展開されて日時がフォーマットされます。

カスタム書式指定子が優先されるケース

ToStringに渡した書式文字列が標準書式指定子の一覧に含まれていない場合、カスタム書式指定子として解釈されます。

カスタム書式指定子は、年や月、日、時、分、秒などのトークンを組み合わせて自由にフォーマットを作成できます。

例えば、"yyyy/MM/dd HH:mm:ss"はカスタム書式指定子であり、標準書式指定子のどれにも該当しません。

そのため、ToStringはこの文字列を解析して、各トークンに対応する日時の値を置き換えます。

カスタム書式指定子は標準書式指定子よりも優先されるため、標準書式指定子と同じ文字でも複数文字であればカスタム書式として扱われます。

例えば、"d"は標準書式指定子ですが、"dd"はカスタム書式指定子です。

サンプルコード:標準とカスタムの違い

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        // 標準書式指定子 "d"
        Console.WriteLine(dt.ToString("d")); // 短い日付形式
        // カスタム書式指定子 "dd"
        Console.WriteLine(dt.ToString("dd")); // 日の2桁表示
        // カスタム書式指定子 "ddd"
        Console.WriteLine(dt.ToString("ddd")); // 曜日略称
        // カスタム書式指定子 "dddd"
        Console.WriteLine(dt.ToString("dddd")); // 曜日フル
    }
}
2025/05/07
07
水
水曜日

この例では、"d"は標準書式指定子として短い日付を表示し、"dd""ddd"はカスタム書式指定子として日や曜日の表示に使われています。

String Interpolationとstring.Formatの使い分け

C#では文字列の中に変数や式を埋め込む方法として、string.Formatと文字列補間(String Interpolation)がよく使われます。

日時のフォーマットでもどちらも利用可能ですが、使い分けのポイントを押さえておくと便利です。

  • string.Format

書式文字列の中に{0}, {1}のようなプレースホルダーを使い、引数の順番に値を埋め込みます。

例:string.Format("{0:yyyy/MM/dd}", DateTime.Now)

  • 文字列補間(String Interpolation)

$"..."の中に{}で囲んだ式を直接書けます。

可読性が高く、変数名や式をそのまま埋め込めるため、直感的です。

例:$"{DateTime.Now:yyyy/MM/dd}"

サンプルコード:両者の比較

using System;
class Program
{
    static void Main()
    {
        DateTime now = DateTime.Now;
        // string.Formatを使ったフォーマット
        string formatted1 = string.Format("現在日時は {0:yyyy年MM月dd日 HH:mm:ss} です。", now);
        Console.WriteLine(formatted1);
        // 文字列補間を使ったフォーマット
        string formatted2 = $"現在日時は {now:yyyy年MM月dd日 HH:mm:ss} です。";
        Console.WriteLine(formatted2);
    }
}
現在日時は 2025年05月08日 03:45:13 です。
現在日時は 2025年05月08日 03:45:13 です。

使い分けのポイント

  • 可読性

文字列補間は変数名や式がそのまま見えるため、コードが読みやすくなります。

  • 複雑なフォーマット

複数の値を順番に埋め込む場合はstring.Formatも有効ですが、文字列補間でも複数の式を埋め込めるため、ほとんどの場合はこちらが推奨されます。

  • パフォーマンス

両者のパフォーマンス差はほとんどなく、どちらを使っても問題ありません。

  • バージョン依存

文字列補間はC# 6.0以降で利用可能です。

古い環境ではstring.Formatを使う必要があります。

以上の理由から、特に新しいコードでは文字列補間を使うことをおすすめします。

日時のフォーマットも{変数:書式}の形で簡単に指定できるため、直感的に書けます。

標準書式指定子カタログ

日付のみを扱う指定子

d 短い日付

dは短い形式の日付を表す標準書式指定子です。

一般的に年月日を数字で簡潔に表示し、カルチャに依存した区切り文字や順序で出力されます。

日本語環境では「2025/05/07」のように表示されることが多いです。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7);
        Console.WriteLine(dt.ToString("d")); // 短い日付形式
    }
}
2025/05/07

この書式はUIのカレンダー表示や簡単な日付表示に適しています。

D 長い日付

Dは長い形式の日付を表します。

曜日や月名が含まれ、より詳細で読みやすい形式になります。

日本語環境では「2025年5月7日水曜日」のように表示されます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7);
        Console.WriteLine(dt.ToString("D")); // 長い日付形式
    }
}
2025年5月7日

カレンダーの詳細表示やレポートの見出しなどに向いています。

時刻を含む指定子

t 短い時刻

tは短い形式の時刻を表します。

時間と分のみを表示し、AM/PM表記はカルチャに依存します。

日本語環境では「8:55 午前」のように表示されます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 0);
        Console.WriteLine(dt.ToString("t")); // 短い時刻形式
    }
}
8:55

簡単な時刻表示やログの概要表示に適しています。

T 長い時刻

Tは長い形式の時刻を表し、秒まで含みます。

日本語環境では「8:55:31 午前」のように表示されます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        Console.WriteLine(dt.ToString("T")); // 長い時刻形式
    }
}
8:55:31

詳細な時刻表示が必要な場合に使います。

日付と時刻の複合指定子

f 短い時刻付き完全日付

fは長い日付形式に短い時刻形式を組み合わせたものです。

日本語環境では「2025年5月7日 8:55」のように秒を含まない時刻が付加されます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        Console.WriteLine(dt.ToString("f")); // 短い時刻付き完全日付
    }
}
2025年5月7日 8:55

イベントの開始日時表示などに適しています。

F 長い時刻付き完全日付

Fは長い日付形式に長い時刻形式を組み合わせたもので、秒まで含みます。

日本語環境では「2025年5月7日水曜日 8:55:31」のように表示されます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        Console.WriteLine(dt.ToString("F")); // 長い時刻付き完全日付
    }
}
2025年5月7日 8:55:31

詳細な日時表示が必要なログやレポートに向いています。

g 一般形式(短)

gは短い日付形式と短い時刻形式を組み合わせた一般的な形式です。

日本語環境では「2025/5/7 8:55」のように秒を含まない時刻が付加されます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        Console.WriteLine(dt.ToString("g")); // 一般形式(短)
    }
}
2025/5/7 8:55

UIの簡易表示や入力フォームの初期表示に適しています。

G 一般形式(長)

Gは短い日付形式と長い時刻形式を組み合わせた一般的な形式で、秒まで含みます。

日本語環境では「2025/5/7 8:55:31」のように表示されます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        Console.WriteLine(dt.ToString("G")); // 一般形式(長)
    }
}
2025/5/7 8:55:31

ログや詳細な日時表示に適しています。

特殊用途の指定子

s ISO 8601に近い並べ替え可能形式

sはソートや比較に適したISO 8601に近い形式で、秒までの日時をyyyy-MM-ddTHH:mm:ssの形で表します。

タイムゾーン情報は含まれません。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        Console.WriteLine(dt.ToString("s")); // ISO 8601に近い形式
    }
}
2025-05-07T08:55:31

ファイル名やデータベースのキーに使いやすい形式です。

o/O ラウンドトリップ形式

oまたはOはラウンドトリップ形式で、日時を完全に復元可能なISO 8601準拠の文字列に変換します。

ミリ秒以下の精度とタイムゾーンオフセットを含みます。

using System;
class Program
{
    static void Main()
    {
        DateTimeOffset dto = new DateTimeOffset(2025, 5, 7, 8, 55, 31, TimeSpan.FromHours(9));
        Console.WriteLine(dto.ToString("o")); // ラウンドトリップ形式
    }
}
2025-05-07T08:55:31.0000000+09:00

日時のシリアライズやAPI通信での日時交換に最適です。

r/R RFC 1123形式

rまたはRはRFC 1123形式で、英語の曜日・月名を使い、UTC時刻で表現します。

HTTPヘッダーなどでよく使われます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31, DateTimeKind.Utc);
        Console.WriteLine(dt.ToString("r")); // RFC 1123形式
    }
}
Wed, 07 May 2025 08:55:31 GMT

インターネットプロトコルでの日時表現に適しています。

u 協定世界時の並べ替え形式

uはUTCの日時をyyyy-MM-dd HH:mm:ssZ形式で表します。

DateTimeKindがUTCである必要があります。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31, DateTimeKind.Utc);
        Console.WriteLine(dt.ToString("u")); // UTCの並べ替え形式
    }
}
2025-05-07 08:55:31Z

UTC基準のログやデータ保存に使われます。

カスタム書式指定子の詳細

年を表すトークン

yyyy 4桁

yyyyは4桁の西暦年を表します。

例えば2025年なら2025と表示されます。

年を完全に表現したい場合に使います。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7);
        Console.WriteLine(dt.ToString("yyyy")); // 4桁の年
    }
}
2025

yy 2桁

yyは西暦年の下2桁を表します。

2025年なら25と表示され、短い年表記が必要な場合に使います。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7);
        Console.WriteLine(dt.ToString("yy")); // 2桁の年
    }
}
25

月を表すトークン

MM 数字2桁

MMは2桁の月を表します。

1月は01、12月は12のようにゼロ埋めされます。

数字で月を表したい場合に使います。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7);
        Console.WriteLine(dt.ToString("MM")); // 2桁の月
    }
}
05

MMM 英語略称

MMMは英語の月の略称を表示します。

例えば5月はMayと表示されます。

英語圏向けのUIなどで使います。

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7);
        Console.WriteLine(dt.ToString("MMM", CultureInfo.InvariantCulture)); // 月の英語略称
    }
}
May

MMMM 英語フル

MMMMは英語の月名をフルで表示します。

5月ならMayですが、例えば9月はSeptemberと表示されます。

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 9, 7);
        Console.WriteLine(dt.ToString("MMMM", CultureInfo.InvariantCulture)); // 月の英語フル名
    }
}
September

日を表すトークン

dd 数字2桁

ddは2桁の日を表します。

1日は01、31日は31のようにゼロ埋めされます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7);
        Console.WriteLine(dt.ToString("dd")); // 2桁の日
    }
}
07

ddd 曜日略称

dddは曜日の略称を表示します。

日本語環境では、英語環境ではWedのように表示されます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7);
        Console.WriteLine(dt.ToString("ddd")); // 曜日略称(日本語環境)
    }
}

dddd 曜日フル

ddddは曜日名をフルで表示します。

日本語環境では水曜日、英語環境ではWednesdayとなります。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7);
        Console.WriteLine(dt.ToString("dddd")); // 曜日フル(日本語環境)
    }
}
水曜日

時を表すトークン

HH 24時間

HHは24時間制の時を2桁で表します。

0時は00、23時は23となります。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 0, 0);
        Console.WriteLine(dt.ToString("HH")); // 24時間制の時
    }
}
08

hh 12時間

hhは12時間制の時を2桁で表します。

午前0時は12、午後1時は01となります。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 13, 0, 0);
        Console.WriteLine(dt.ToString("hh")); // 12時間制の時
    }
}
01

分・秒・ミリ秒

mm 分

mmは分を2桁で表します。

0分は00、59分は59となります。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 5, 0);
        Console.WriteLine(dt.ToString("mm")); // 分
    }
}
05

ss 秒

ssは秒を2桁で表します。

0秒は00、59秒は59となります。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 5, 9);
        Console.WriteLine(dt.ToString("ss")); // 秒
    }
}
09

fff ミリ秒

fffはミリ秒を3桁で表します。

0ミリ秒は000、999ミリ秒は999となります。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 5, 9, 123);
        Console.WriteLine(dt.ToString("fff")); // ミリ秒
    }
}
123

その他の便利トークン

tt AM/PM

ttは午前・午後を表します。

日本語環境では午前または午後、英語環境ではAMまたはPMとなります。

using System;
class Program
{
    static void Main()
    {
        DateTime dtMorning = new DateTime(2025, 5, 7, 8, 0, 0);
        DateTime dtEvening = new DateTime(2025, 5, 7, 20, 0, 0);
        Console.WriteLine(dtMorning.ToString("tt")); // 午前
        Console.WriteLine(dtEvening.ToString("tt")); // 午後
    }
}
午前
午後

zzz タイムゾーンオフセット

zzzはタイムゾーンのオフセットを±hh:mm形式で表示します。

DateTimeOffsetでよく使われます。

using System;
class Program
{
    static void Main()
    {
        DateTimeOffset dto = new DateTimeOffset(2025, 5, 7, 8, 0, 0, TimeSpan.FromHours(9));
        Console.WriteLine(dto.ToString("zzz")); // タイムゾーンオフセット
    }
}
+09:00

エスケープ文字とリテラル扱い

カスタム書式指定子の中で特定の文字をそのまま表示したい場合は、シングルクォーテーション'またはダブルクォーテーション"で囲みます。

また、バックスラッシュ\で直後の文字をエスケープできます。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 5, 9);
        // 「年」「月」「日」をリテラルとして表示
        Console.WriteLine(dt.ToString("yyyy'年'MM'月'dd'日' HH:mm:ss"));
        // バックスラッシュで「T」をリテラル表示
        Console.WriteLine(dt.ToString("yyyy-MM-dd\\THH:mm:ss"));
    }
}
2025年05月07日 08:05:09
2025-05-07T08:05:09

このようにリテラルを使うことで、日時のフォーマットに任意の文字列を挿入できます。

カルチャとローカリゼーション

CultureInfoの適用方法

日時のフォーマットはカルチャ(文化圏)によって表示形式が大きく異なります。

C#ではSystem.Globalization.CultureInfoクラスを使ってカルチャを指定し、日時のローカライズを制御できます。

スレッドカルチャを変更する方法

アプリケーション全体でカルチャを統一したい場合は、現在のスレッドのカルチャを変更します。

これにより、ToStringなどのメソッドでカルチャを明示的に指定しなくても、スレッドのカルチャに従った表示になります。

using System;
using System.Globalization;
using System.Threading;
class Program
{
    static void Main()
    {
        // 現在のスレッドのカルチャを日本語(日本)に設定
        Thread.CurrentThread.CurrentCulture = new CultureInfo("ja-JP");
        Thread.CurrentThread.CurrentUICulture = new CultureInfo("ja-JP");
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        Console.WriteLine(dt.ToString("D")); // 長い日付形式(日本語)
    }
}
2025年5月7日

この方法は、アプリケーション全体のカルチャを統一したい場合に便利ですが、マルチスレッド環境ではスレッドごとに設定が必要です。

個別呼び出しでカルチャを渡す方法

特定の日時フォーマットだけ異なるカルチャで表示したい場合は、ToStringメソッドの第2引数にCultureInfoを渡します。

これにより、スレッドのカルチャに影響を与えずにローカライズできます。

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        // 日本語カルチャで表示
        Console.WriteLine(dt.ToString("D", new CultureInfo("ja-JP")));
        // 英語(米国)カルチャで表示
        Console.WriteLine(dt.ToString("D", new CultureInfo("en-US")));
    }
}
2025年5月7日
Wednesday, May 7, 2025

この方法は、UIの多言語対応や一部の表示だけ異なるカルチャにしたい場合に適しています。

和暦(令和・平成など)表示

日本の和暦を使った日時表示は、CultureInfoCalendarクラスを組み合わせて実現します。

特にJapaneseCalendarを使うことで、元号を含む和暦表記が可能です。

JapaneseCalendarの使用例

CultureInfoDateTimeFormat.CalendarプロパティにJapaneseCalendarを設定し、和暦表示用のカルチャを作成します。

これにより、ToStringで元号付きの和暦が表示されます。

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7);
        CultureInfo ci = new CultureInfo("ja-JP");
        ci.DateTimeFormat.Calendar = new JapaneseCalendar();
        // 和暦の長い日付形式
        Console.WriteLine(dt.ToString("gg y年M月d日", ci));
    }
}
令和 7年5月7日

ここでggは元号の略称(令和)、yは元号年を表します。

元号変換で注意すべきポイント

  • 元号の切り替え日

元号は特定の日付で切り替わるため、日付によって元号が変わります。

JapaneseCalendarはこれを正しく処理しますが、元号切り替え直前後の日時を扱う際は注意が必要です。

  • 元号の略称と正式名称

ggは元号の略称を表示しますが、正式名称を表示したい場合はカスタム処理が必要です。

  • .NETのバージョン依存

元号の情報は.NETのバージョンによって更新されるため、最新の元号に対応しているか確認してください。

カルチャ依存の落とし穴

月名・曜日名の言語差

標準書式やカスタム書式で月名や曜日名を表示すると、カルチャによって言語が変わります。

例えば、MMMMは日本語環境では「5月」、英語環境では「May」となります。

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7);
        Console.WriteLine(dt.ToString("dddd MMMM", new CultureInfo("ja-JP"))); // 日本語
        Console.WriteLine(dt.ToString("dddd MMMM", new CultureInfo("en-US"))); // 英語
    }
}
水曜日 5月
Wednesday May

UIで多言語対応する場合は、カルチャを適切に指定しないと意図しない言語で表示されることがあります。

数字表記と区切り文字の違い

カルチャによって数字の表記方法や日付・時刻の区切り文字が異なります。

例えば日本語では年月日の区切りに「年」「月」「日」が使われますが、英語圏ではスラッシュやハイフンが使われます。

また、数字の全角・半角や桁区切りのカンマの有無もカルチャ依存です。

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        Console.WriteLine(dt.ToString("D", new CultureInfo("ja-JP"))); // 日本語
        Console.WriteLine(dt.ToString("D", new CultureInfo("fr-FR"))); // フランス語
    }
}
2025年5月7日水曜日
mercredi 7 mai 2025

このようにカルチャによって日付の順序や区切り文字が変わるため、表示形式を固定したい場合はカスタム書式指定子を使い、カルチャを明示的に指定することが重要です。

実務で役立つフォーマットパターン集

ログタイムスタンプの統一

ログファイルやシステムログで日時を扱う際は、フォーマットの統一が重要です。

日時の表記がバラバラだと解析やトラブルシューティングが難しくなるため、一定のルールを設けることが多いです。

ファイルローテーション向け

ログファイルのローテーション(世代管理)を行う場合、ファイル名に日時を含めることが一般的です。

このとき、日時のフォーマットはファイル名として使える文字だけを使い、かつ時系列でソート可能な形式にする必要があります。

推奨フォーマットは以下のようなISO 8601準拠の形式です。

yyyyMMdd_HHmmss 例:log_20250507_085531.txt

using System;
class Program
{
    static void Main()
    {
        DateTime now = DateTime.Now;
        string fileName = $"log_{now:yyyyMMdd_HHmmss}.txt";
        Console.WriteLine(fileName);
    }
}
log_20250507_085531.txt

この形式はファイル名として安全で、ファイルエクスプローラーやコマンドラインでのソートも日時順になります。

プロセス間での共有

複数のプロセスやサービス間でログを共有・統合する場合は、タイムゾーンを明示したUTCベースの日時を使うことが望ましいです。

これにより、異なる環境のログを時系列で正確に並べられます。

推奨フォーマットはラウンドトリップ形式oのUTC表記です。

例:2025-05-07T23:55:31.0000000Z

using System;
class Program
{
    static void Main()
    {
        DateTime utcNow = DateTime.UtcNow;
        string timestamp = utcNow.ToString("o");
        Console.WriteLine(timestamp);
    }
}
2025-05-07T23:55:31.0000000Z

この形式はISO 8601に準拠し、タイムゾーン情報も含むため、ログ解析ツールや分散システムでの利用に適しています。

UI表示の見やすさ向上

ユーザーインターフェースで日時を表示する際は、見やすさやユーザーの好みに応じてフォーマットを調整することが重要です。

短縮表記とフル表記の切替

画面のスペースやコンテキストに応じて、短縮表記(例:5/7 8:55)とフル表記(例:2025年5月7日水曜日 8:55:31)を切り替えることがあります。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = DateTime.Now;
        // 短縮表記
        string shortFormat = dt.ToString("M月 HH:mm");
        Console.WriteLine(shortFormat);
        // フル表記
        string fullFormat = dt.ToString("yyyy年M月d日 dddd HH:mm:ss");
        Console.WriteLine(fullFormat);
    }
}
5月 08:55
2025年5月7日 水曜日 08:55:31

短縮表記は一覧表示やスペースが限られた場所に適し、フル表記は詳細画面やレポートで使います。

ユーザー設定によるカスタマイズ

ユーザーが日時の表示形式を選べるようにする場合、カスタム書式指定子を使って柔軟に対応します。

例えば、設定画面でフォーマット文字列を入力させ、それをToStringに渡す形です。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = DateTime.Now;
        // ユーザー設定のフォーマット例
        string userFormat = "yyyy/MM/dd HH:mm";
        Console.WriteLine(dt.ToString(userFormat));
    }
}
2025/05/07 08:55

ユーザーの好みに合わせて表示を変えられるため、UX向上に役立ちます。

ファイル名に使える安全な書式

ファイル名に日時を含める場合、OSやファイルシステムで禁止されている文字を避ける必要があります。

OS間互換性を保つ禁則文字回避

Windowsでは\/:*?"<>|などの文字がファイル名に使えません。

日時の区切りにコロン:を使うHH:mm:ss形式は避け、代わりにアンダースコアやハイフンを使います。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = DateTime.Now;
        // コロンを使わずに安全なファイル名を作成
        string safeFileName = dt.ToString("yyyyMMdd_HHmmss") + ".log";
        Console.WriteLine(safeFileName);
    }
}
20250507_085531.log

この形式はWindowsだけでなくLinuxやmacOSでも問題なく使えます。

時系列ソートを考慮した形

ファイル名で時系列ソートを正しく行うには、年月日から秒までの順に並べることが重要です。

yyyyMMddHHmmssのように連続した数字列にするか、区切り文字を入れても年月日→時分秒の順序を守ります。

using System;
class Program
{
    static void Main()
    {
        DateTime dt = DateTime.Now;
        string fileName = dt.ToString("yyyy-MM-dd_HH-mm-ss") + ".log";
        Console.WriteLine(fileName);
    }
}
2025-05-07_08-55-31.log

こうすることで、ファイル一覧を名前順に並べるだけで日時順に並べられます。

データベース/JSON連携

日時データをデータベースやJSONで扱う際は、フォーマットの統一とタイムゾーンの明示が重要です。

ISO 8601での保存

ISO 8601形式(例:2025-05-07T08:55:31Z)は国際的に標準化された日時表記で、多くのシステムやAPIで推奨されています。

DateTimeDateTimeOffsetToString("o")で簡単に生成できます。

using System;
class Program
{
    static void Main()
    {
        DateTimeOffset dto = DateTimeOffset.UtcNow;
        string iso8601 = dto.ToString("o");
        Console.WriteLine(iso8601);
    }
}
2025-05-07T08:55:31.0000000Z

JSONやデータベースにこの形式で保存すると、日時の解釈ミスを防げます。

タイムゾーンの明示

日時を保存・交換する際は、必ずタイムゾーンやオフセットを含めることが重要です。

DateTimeKindUtcであればZが付き、DateTimeOffsetならオフセットが付与されます。

using System;
class Program
{
    static void Main()
    {
        DateTime local = DateTime.Now;
        DateTime utc = DateTime.UtcNow;
        DateTimeOffset dto = new DateTimeOffset(local);
        Console.WriteLine("Local: " + local.ToString("o"));  // ローカル時刻(オフセットなし)
        Console.WriteLine("UTC: " + utc.ToString("o"));      // UTC時刻(Z付き)
        Console.WriteLine("Offset: " + dto.ToString("o"));   // オフセット付き
    }
}
Local: 2025-05-08T03:48:27.4518588+09:00
UTC: 2025-05-07T18:48:27.4554019Z
Offset: 2025-05-08T03:48:27.4518588+09:00

ローカル時刻はタイムゾーン情報がないため、保存や共有には不向きです。

UTCやオフセット付きの日時を使うことを推奨します。

DateTimeOffsetとタイムゾーン

時差情報付き日時のフォーマット

DateTimeOffsetは日時に加えてUTCからのオフセット(時差)を保持しているため、時差情報を含めた日時のフォーマットが可能です。

これにより、異なるタイムゾーンの日時を正確に表現・比較できます。

ToStringメソッドにカスタム書式指定子zzzを使うと、±hh:mm形式のオフセットを表示できます。

また、標準書式指定子のo(ラウンドトリップ形式)を使うと、日時とオフセットをISO 8601形式で一括して表現します。

using System;
class Program
{
    static void Main()
    {
        DateTimeOffset dto = new DateTimeOffset(2025, 5, 7, 8, 55, 31, TimeSpan.FromHours(9));
        // カスタム書式でオフセットを含めて表示
        Console.WriteLine(dto.ToString("yyyy-MM-dd HH:mm:ss zzz"));
        // ラウンドトリップ形式で表示(ISO 8601)
        Console.WriteLine(dto.ToString("o"));
    }
}
2025-05-07 08:55:31 +09:00
2025-05-07T08:55:31.0000000+09:00

このように、DateTimeOffsetはオフセットを含む日時を簡単にフォーマットできるため、タイムゾーンを意識した日時管理に適しています。

ToUniversalTimeとToLocalTime

DateTimeOffsetには、日時をUTC(協定世界時)やローカルタイムに変換するメソッドが用意されています。

  • ToUniversalTime()

現在の日時をUTCに変換します。

オフセットが+09:00なら、9時間引いたUTC時刻が返されます。

戻り値はDateTimeOffset型で、オフセットはTimeSpan.Zero(UTC)になります。

  • ToLocalTime()

UTCや他のオフセットの日時を、実行環境のローカルタイムゾーンに変換します。

戻り値はDateTimeOffset型で、オフセットはローカルタイムゾーンのオフセットに変わります。

using System;
class Program
{
    static void Main()
    {
        DateTimeOffset dto = new DateTimeOffset(2025, 5, 7, 8, 55, 31, TimeSpan.FromHours(9));
        DateTimeOffset utc = dto.ToUniversalTime();
        DateTimeOffset local = utc.ToLocalTime();
        Console.WriteLine("元の日時: " + dto.ToString("o"));
        Console.WriteLine("UTC日時: " + utc.ToString("o"));
        Console.WriteLine("ローカル日時: " + local.ToString("o"));
    }
}
元の日時: 2025-05-07T08:55:31.0000000+09:00
UTC日時: 2025-05-06T23:55:31.0000000+00:00
ローカル日時: 2025-05-07T08:55:31.0000000+09:00

この例では、元の日時は日本標準時(+09:00)で、ToUniversalTimeでUTCに変換し、ToLocalTimeで再びローカルタイムに戻しています。

実行環境のタイムゾーンによってToLocalTimeの結果は変わる点に注意してください。

IANAタイムゾーンデータを使うときの注意

.NETの標準DateTimeOffsetTimeZoneInfoはWindowsのタイムゾーンID(例:”Tokyo Standard Time”)を使いますが、LinuxやmacOSではIANAタイムゾーンデータ(例:”Asia/Tokyo”)が使われることが多いです。

この違いにより、クロスプラットフォーム開発時に注意が必要です。

  • WindowsとIANAのタイムゾーンIDの違い

Windowsは独自のタイムゾーンIDを持ち、IANAとは名前が異なります。

例えば東京はWindowsでは"Tokyo Standard Time"、IANAでは"Asia/Tokyo"です。

  • 変換方法

.NET 6以降ではTimeZoneConverterなどの外部ライブラリを使うことで、IANAとWindowsのタイムゾーンIDを相互変換できます。

標準APIだけでは直接変換できないため、ライブラリの導入が推奨されます。

  • TimeZoneInfoの利用

TimeZoneInfo.FindSystemTimeZoneByIdに渡すIDはOSによって異なるため、クロスプラットフォーム対応時はIDの管理に注意が必要です。

using System;
using TimeZoneConverter; // NuGetでTimeZoneConverterをインストール
class Program
{
    static void Main()
    {
        // IANAタイムゾーンID
        string ianaId = "Asia/Tokyo";
        // WindowsタイムゾーンIDに変換
        string windowsId = TZConvert.IanaToWindows(ianaId);
        TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById(windowsId);
        DateTime utcNow = DateTime.UtcNow;
        DateTime localTime = TimeZoneInfo.ConvertTimeFromUtc(utcNow, tz);
        Console.WriteLine($"IANA ID: {ianaId}");
        Console.WriteLine($"Windows ID: {windowsId}");
        Console.WriteLine($"ローカル時間: {localTime}");
    }
}
IANA ID: Asia/Tokyo
Windows ID: Tokyo Standard Time
ローカル時間: 2025/05/07 17:55:31

IANAタイムゾーンを使う場合は、Windows環境での互換性を考慮し、ID変換を行うか、環境ごとに適切なIDを使い分ける必要があります。

特にクロスプラットフォームの.NETアプリケーションでは、TimeZoneConverterのようなライブラリを活用すると便利です。

パフォーマンスとメモリ効率

日時のフォーマットは頻繁に行われる処理の一つであり、特に大量のログ出力やリアルタイム処理ではパフォーマンスとメモリ効率が重要になります。

ここでは、DateTimeDateTimeOffsetのフォーマット処理とStringBuilderの比較、さらにフォーマット文字列のキャッシュ戦略について解説します。

StringBuilderとの比較

DateTime.ToStringDateTimeOffset.ToStringは内部で文字列を生成しますが、単純な日時フォーマットではStringBuilderを使うよりも高速かつ効率的な場合が多いです。

これは、ToStringメソッドが最適化されており、必要な文字数を事前に計算してバッファを確保するためです。

一方、複数の文字列を連結したり、複雑な文字列操作を行う場合はStringBuilderの方が効率的です。

例えば、日時のフォーマット結果に他の文字列を頻繁に追加するケースです。

サンプルコード:ToStringとStringBuilderの比較

using System;
using System.Text;
using System.Diagnostics;
class Program
{
    static void Main()
    {
        DateTime now = DateTime.Now;
        int iterations = 1000000;
        // DateTime.ToStringの計測
        var sw1 = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            string s = now.ToString("yyyy-MM-dd HH:mm:ss");
        }
        sw1.Stop();
        Console.WriteLine($"DateTime.ToString: {sw1.ElapsedMilliseconds} ms");
        // StringBuilderを使った計測
        var sw2 = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            var sb = new StringBuilder();
            sb.Append(now.Year.ToString("D4"));
            sb.Append("-");
            sb.Append(now.Month.ToString("D2"));
            sb.Append("-");
            sb.Append(now.Day.ToString("D2"));
            sb.Append(" ");
            sb.Append(now.Hour.ToString("D2"));
            sb.Append(":");
            sb.Append(now.Minute.ToString("D2"));
            sb.Append(":");
            sb.Append(now.Second.ToString("D2"));
            string s = sb.ToString();
        }
        sw2.Stop();
        Console.WriteLine($"StringBuilder: {sw2.ElapsedMilliseconds} ms");
    }
}
DateTime.ToString: 106 ms
StringBuilder: 129 ms

この結果から、単純な日時フォーマットではToStringの方が高速であることがわかります。

StringBuilderは複雑な文字列操作に向いていますが、単純な日時フォーマットには過剰な手段となる場合があります。

フォーマットキャッシュ戦略

大量の日時フォーマットを繰り返す場合、フォーマット文字列やカルチャ情報の取得・解析コストが無視できなくなります。

これを軽減するために、フォーマット文字列やカルチャごとのフォーマット情報をキャッシュする戦略が有効です。

静的フィールドに書式文字列を保持

フォーマット文字列は不変であることが多いため、毎回文字列リテラルを渡すのではなく、静的フィールドに保持して使い回すことで、文字列の再生成やGC負荷を減らせます。

using System;
class Program
{
    private static readonly string DateFormat = "yyyy-MM-dd HH:mm:ss";
    static void Main()
    {
        DateTime now = DateTime.Now;
        for (int i = 0; i < 1000; i++)
        {
            string formatted = now.ToString(DateFormat);
            Console.WriteLine(formatted);
        }
    }
}

このように静的フィールドに書式文字列を保持すると、文字列のインスタンス生成が減り、メモリ効率が向上します。

カルチャごとのキャッシュ

CultureInfoDateTimeFormatInfoの取得や解析もコストがかかるため、カルチャごとにフォーマット情報をキャッシュしておくとパフォーマンスが改善します。

特に多言語対応アプリケーションで複数カルチャを扱う場合に有効です。

using System;
using System.Collections.Concurrent;
using System.Globalization;
class DateFormatter
{
    private static readonly ConcurrentDictionary<string, DateTimeFormatInfo> FormatInfoCache = new();
    public static string FormatDate(DateTime dt, string format, string cultureName)
    {
        var formatInfo = FormatInfoCache.GetOrAdd(cultureName, name =>
        {
            var ci = new CultureInfo(name);
            return ci.DateTimeFormat;
        });
        return dt.ToString(format, formatInfo);
    }
}
class Program
{
    static void Main()
    {
        DateTime now = DateTime.Now;
        string formattedJa = DateFormatter.FormatDate(now, "D", "ja-JP");
        string formattedEn = DateFormatter.FormatDate(now, "D", "en-US");
        Console.WriteLine(formattedJa);
        Console.WriteLine(formattedEn);
    }
}
2025年5月8日
Thursday, May 8, 2025

この例では、ConcurrentDictionaryを使ってカルチャごとのDateTimeFormatInfoをキャッシュし、同じカルチャのフォーマット処理を効率化しています。

これらの方法を組み合わせることで、日時フォーマットのパフォーマンスとメモリ効率を高め、特に大量データ処理や高負荷環境での安定した動作を実現できます。

よくあるエラーとデバッグ方法

日時のフォーマットを扱う際には、細かなミスや誤解によって意図しない結果になることがあります。

ここでは、よくあるエラーとその原因、デバッグのポイントを具体例とともに解説します。

大文字小文字のミス

日時のカスタム書式指定子は大文字・小文字で意味が異なるものが多いため、誤った大文字小文字を使うと期待した表示にならないことがあります。

例えば、MMは「月」を2桁で表しますが、mmは「分」を2桁で表します。

同様に、HHは24時間制の時、hhは12時間制の時を表します。

サンプルコード:大文字小文字の違い

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 14, 9, 5);
        // 月(大文字MM)と分(小文字mm)
        Console.WriteLine(dt.ToString("MM")); // 05
        Console.WriteLine(dt.ToString("mm")); // 09
        // 24時間(HH)と12時間(hh)
        Console.WriteLine(dt.ToString("HH")); // 14
        Console.WriteLine(dt.ToString("hh")); // 02
    }
}
05
09
14
02

デバッグポイント

  • 書式指定子の大文字小文字を正確に確認する
  • ドキュメントやリファレンスで意味を再確認する
  • 意図した時間表記(12時間制か24時間制か)を明確にする

リテラル文字のエスケープ忘れ

カスタム書式指定子の中で、年月日や時刻の間に特定の文字(例:「年」「月」「日」や「T」など)をそのまま表示したい場合は、リテラルとしてエスケープする必要があります。

これを忘れると、書式指定子として解釈されてしまい、意図しない結果になります。

サンプルコード:エスケープ忘れの例と修正例

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 5, 9);
        // エスケープ忘れ("年"が書式指定子として誤解される)
        Console.WriteLine(dt.ToString("yyyy年MM月dd日 HH:mm:ss")); // 正しく表示されるが、年・月・日はリテラルとして扱われている
        // 明示的にリテラルをエスケープ(シングルクォーテーションで囲む)
        Console.WriteLine(dt.ToString("yyyy'年'MM'月'dd'日' HH:mm:ss"));
        // バックスラッシュでエスケープ(例:Tをリテラル表示)
        Console.WriteLine(dt.ToString("yyyy-MM-dd\\THH:mm:ss"));
    }
}
2025年05月07日 08:05:09
2025年05月07日 08:05:09
2025-05-07T08:05:09

デバッグポイント

  • リテラル文字はシングルクォーテーション'またはバックスラッシュ\でエスケープする
  • 書式文字列の中に意図しない書式指定子が混入していないか確認する
  • 出力結果が想定と異なる場合は、リテラルの扱いを疑う

日付と時刻の桁数不足

カスタム書式指定子は桁数によって表示形式が変わります。

例えば、dは1桁または2桁の日を表示しますが、ddは必ず2桁(ゼロ埋め)で表示します。

桁数が不足していると、見た目が揃わず不自然になることがあります。

サンプルコード:桁数の違い

using System;
class Program
{
    static void Main()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 5, 9);
        // 1桁または2桁表示
        Console.WriteLine(dt.ToString("d"));  // 7
        Console.WriteLine(dt.ToString("dd")); // 07
        // 時間も同様
        Console.WriteLine(dt.ToString("H"));  // 8
        Console.WriteLine(dt.ToString("HH")); // 08
    }
}
7
07
8
08

デバッグポイント

  • 表示の揃えが必要な場合は桁数を指定する(例:dd, HH)
  • ユーザーインターフェースの見た目を考慮して桁数を調整する
  • 桁数不足で意図しない表示になっていないか確認する

UTCとローカルの取り違え

日時のKindプロパティDateTimeKindDateTimeOffsetのオフセットを正しく理解しないと、UTCとローカル時刻を混同してしまい、誤った日時表示や計算ミスが発生します。

よくあるミス例

  • UTC日時をローカル時刻として表示してしまう
  • ローカル日時をUTCとして扱い、時差がずれる
  • DateTimeKindUnspecifiedのまま処理する

サンプルコード:UTCとローカルの違い

using System;
class Program
{
    static void Main()
    {
        DateTime utcNow = DateTime.UtcNow;
        DateTime localNow = DateTime.Now;
        DateTime unspecified = new DateTime(2025, 5, 7, 8, 55, 31, DateTimeKind.Unspecified);
        Console.WriteLine("UTC Now: " + utcNow.ToString("o"));
        Console.WriteLine("Local Now: " + localNow.ToString("o"));
        Console.WriteLine("Unspecified: " + unspecified.ToString("o"));
        // UTC日時をローカルに変換
        Console.WriteLine("UTC to Local: " + utcNow.ToLocalTime().ToString("o"));
        // ローカル日時をUTCに変換
        Console.WriteLine("Local to UTC: " + localNow.ToUniversalTime().ToString("o"));
    }
}
UTC Now: 2025-05-07T23:55:31.0000000Z
Local Now: 2025-05-07T08:55:31.0000000+09:00
Unspecified: 2025-05-07T08:55:31.0000000
UTC to Local: 2025-05-08T08:55:31.0000000+09:00
Local to UTC: 2025-05-07T23:55:31.0000000Z

デバッグポイント

  • DateTime.Kindの値を常に意識する
  • UTCとローカルの変換はToUniversalTime()ToLocalTime()を使う
  • DateTimeOffsetを使うと時差情報が明示的になるため誤用を減らせる
  • ログやAPIで日時を扱う際は、UTC基準で統一することを検討する

これらのポイントを押さえることで、日時フォーマットに関するトラブルを減らし、正確で意図した表示を実現できます。

エラーが発生した場合は、まず書式指定子の大文字小文字、リテラルの扱い、桁数、そして日時のタイムゾーン情報を順に確認すると効率的です。

.NET 6以降の新しい型

.NET 6から新たに導入されたDateOnlyTimeOnlyは、日付のみ、または時刻のみを扱うための型です。

これにより、従来のDateTimeで日付と時刻を一緒に扱う際の曖昧さを解消し、より明確に用途を分けられるようになりました。

DateOnlyのフォーマット

DateOnlyは日付情報だけを持ち、時刻情報は含みません。

フォーマット方法はDateTimeと似ていますが、ToStringメソッドで使える書式指定子は日付に関するものに限定されます。

using System;
class Program
{
    static void Main()
    {
        DateOnly date = new DateOnly(2025, 5, 7);
        // 標準書式指定子
        Console.WriteLine(date.ToString("d"));  // 短い日付形式
        Console.WriteLine(date.ToString("D"));  // 長い日付形式
        // カスタム書式指定子
        Console.WriteLine(date.ToString("yyyy/MM/dd"));
        Console.WriteLine(date.ToString("dddd, MMMM dd, yyyy"));
    }
}
2025/05/07
2025年5月7日
2025/05/07
水曜日, 5月 07, 2025

DateOnlyは時刻を持たないため、HHmmなどの時刻に関する書式指定子は使えません。

日付部分のフォーマットに特化しているため、日付だけを扱うUIやデータ処理に適しています。

TimeOnlyのフォーマット

TimeOnlyは時刻情報だけを持ち、日付情報は含みません。

こちらもToStringで時刻に関する標準・カスタム書式指定子を使ってフォーマットできます。

using System;
class Program
{
    static void Main()
    {
        TimeOnly time = new TimeOnly(8, 55, 31);
        // 標準書式指定子
        Console.WriteLine(time.ToString("t"));  // 短い時刻形式
        Console.WriteLine(time.ToString("T"));  // 長い時刻形式
        // カスタム書式指定子
        Console.WriteLine(time.ToString("HH:mm:ss"));
        Console.WriteLine(time.ToString("hh:mm tt"));
    }
}
8:55
8:55:31
08:55:31
08:55 午前

TimeOnlyは日付を持たないため、yyyyMMなどの日付に関する書式指定子は使えません。

時刻だけを扱うシナリオ、例えばアラーム設定やタイマー表示に適しています。

従来型との相互変換

DateOnlyTimeOnlyDateTimeと相互に変換可能です。

これにより、既存のDateTimeベースのAPIやライブラリとも連携しやすくなっています。

DateOnly ⇔ DateTime

  • DateOnlyからDateTimeへは、DateOnly.ToDateTimeメソッドを使い、時刻を指定して変換します。時刻を指定しない場合は0時(00:00:00)になります
  • DateTimeからDateOnlyへは、DateOnly.FromDateTime静的メソッドを使います。時刻部分は切り捨てられます
using System;
class Program
{
    static void Main()
    {
        DateOnly dateOnly = new DateOnly(2025, 5, 7);
        DateTime dtFromDateOnly = dateOnly.ToDateTime(new TimeOnly(8, 30, 0));
        Console.WriteLine(dtFromDateOnly);  // 2025/05/07 08:30:00
        DateTime dt = new DateTime(2025, 5, 7, 15, 45, 0);
        DateOnly dateOnlyFromDt = DateOnly.FromDateTime(dt);
        Console.WriteLine(dateOnlyFromDt);  // 2025/05/07
    }
}
2025/05/07 8:30:00
2025/05/07

TimeOnly ⇔ DateTime

  • TimeOnlyからDateTimeへは、TimeOnly.ToDateTimeメソッドを使い、日付を指定して変換します。日付を指定しない場合はDateTime.MinValueの日付が使われます
  • DateTimeからTimeOnlyへは、TimeOnly.FromDateTime静的メソッドを使います。日付部分は無視され、時刻だけが抽出されます
using System;

class Program
{
    static void Main()
    {
        TimeOnly timeOnly = new TimeOnly(8, 55, 31);
        DateTime dtFromTimeOnly = new DateOnly(2025, 5, 7).ToDateTime(timeOnly);
        Console.WriteLine(dtFromTimeOnly);  // 2025/05/07 08:55:31

        DateTime dt = new DateTime(2025, 5, 7, 15, 45, 0);
        TimeOnly timeOnlyFromDt = TimeOnly.FromDateTime(dt);
        Console.WriteLine(timeOnlyFromDt);  // 15:45:00
    }
}
2025/05/07 08:55:31
15:45:00

これらの相互変換を活用することで、DateOnlyTimeOnlyの利便性を保ちつつ、従来のDateTimeベースのコードやAPIとの互換性を確保できます。

用途に応じて適切な型を選び、日時の扱いをより明確にしましょう。

カスタムIFormatProviderの活用

日時のフォーマットをより柔軟に制御したい場合、IFormatProviderインターフェースを実装したカスタムフォーマットプロバイダーを作成する方法があります。

これにより、標準のカルチャ情報に依存せず、独自のフォーマットルールやロジックを組み込むことが可能です。

実装パターン

IFormatProviderは、GetFormatメソッドを持つインターフェースで、フォーマットに必要な情報を提供します。

日時のフォーマットに関しては、ICustomFormatterインターフェースと組み合わせて使うことが一般的です。

以下は、IFormatProviderICustomFormatterを実装し、特定のカスタム書式文字列に対して独自のフォーマットを返す例です。

using System;
using System.Globalization;

class CustomDateFormatProvider : IFormatProvider, ICustomFormatter
{
    public object? GetFormat(Type? formatType)
    {
        if (formatType == typeof(ICustomFormatter))
            return this;
        return null;
    }

    public string Format(string? format, object? arg, IFormatProvider? formatProvider)
    {
        if (arg is DateTime dt)
        {
            if (format == "X") // 独自のカスタム書式指定子「X」
            {
                // 年月日を逆順で表示 (例: 07-05-2025)
                return $"{dt:dd-MM-yyyy}";
            }
            else if (format == "Y")
            {
                // 年と月だけ表示 (例: 2025/05)
                return $"{dt:yyyy/MM}";
            }
        }

        // null や標準フォーマットは標準処理に委譲
        if (arg is IFormattable formattable)
        {
            return formattable.ToString(format, CultureInfo.CurrentCulture) ?? string.Empty;
        }

        return arg?.ToString() ?? string.Empty;
    }
}

class Program
{
    static void Main()
    {
        DateTime now = new DateTime(2025, 5, 7);
        var provider = new CustomDateFormatProvider();

        // 独自書式「X」を使う
        Console.WriteLine(string.Format(provider, "{0:X}", now)); // 出力: 07-05-2025

        // 独自書式「Y」を使う
        Console.WriteLine(string.Format(provider, "{0:Y}", now)); // 出力: 2025/05

        // 標準書式「D」は通常どおりカルチャに依存
        Console.WriteLine(now.ToString("D", CultureInfo.CurrentCulture)); // 例: 2025年5月7日水曜日 (環境依存)
    }
}
07-05-2025
2025/05
2025年5月7日

この例では、CustomDateFormatProviderXYという独自の書式指定子を解釈し、それ以外は標準のカルチャに委譲しています。

これにより、特定のフォーマットだけをカスタマイズしつつ、他は通常通りの動作を維持できます。

システム全体で統一する方法

カスタムIFormatProviderをシステム全体で統一的に使いたい場合、以下のような方法があります。

グローバルなラッパー関数を作成する

日時のフォーマットを行う箇所をすべて直接ToStringで呼ぶのではなく、共通のヘルパーメソッドを用意し、そこにカスタムフォーマットプロバイダーを組み込みます。

using System;
static class DateTimeFormatter
{
    private static readonly CustomDateFormatProvider Provider = new CustomDateFormatProvider();
    public static string Format(DateTime dt, string format)
    {
        return dt.ToString(format, Provider);
    }
}
class Program
{
    static void Main()
    {
        DateTime now = new DateTime(2025, 5, 7);
        Console.WriteLine(DateTimeFormatter.Format(now, "X")); // 07-05-2025
        Console.WriteLine(DateTimeFormatter.Format(now, "Y")); // 2025/05
        Console.WriteLine(DateTimeFormatter.Format(now, "D")); // 2025年5月7日水曜日
    }
}

この方法なら、フォーマットのルールを一元管理でき、将来的な変更も容易です。

拡張メソッドでラップする

DateTime型に対して拡張メソッドを作成し、カスタムフォーマットプロバイダーを自動的に適用する方法もあります。

using System;
static class DateTimeExtensions
{
    private static readonly CustomDateFormatProvider Provider = new CustomDateFormatProvider();
    public static string ToCustomString(this DateTime dt, string format)
    {
        return dt.ToString(format, Provider);
    }
}
class Program
{
    static void Main()
    {
        DateTime now = new DateTime(2025, 5, 7);
        Console.WriteLine(now.ToCustomString("X")); // 07-05-2025
        Console.WriteLine(now.ToCustomString("Y")); // 2025/05
        Console.WriteLine(now.ToCustomString("D")); // 2025年5月7日水曜日
    }
}

拡張メソッドを使うことで、既存のコードに影響を与えずにカスタムフォーマットを導入しやすくなります。

DIコンテナや設定で注入する

大規模なアプリケーションでは、依存性注入(DI)コンテナを使ってカスタムフォーマットプロバイダーを注入し、日時フォーマットのルールをサービスとして管理する方法もあります。

これにより、環境やユーザー設定に応じて動的にフォーマットを切り替えられます。

カスタムIFormatProviderを活用することで、標準のカルチャ情報に縛られない柔軟な日時フォーマットが可能になります。

システム全体で統一的に使うためには、共通のラッパーや拡張メソッドを用意し、フォーマットの一元管理を心がけるとよいでしょう。

外部ライブラリによる拡張

C#の標準ライブラリは日時のフォーマットに関して多くの機能を提供していますが、より高度な日時処理や特殊なカレンダー対応、曖昧な日付の扱いなどが必要な場合は外部ライブラリを活用すると便利です。

ここでは代表的なライブラリであるNodaTimeのフォーマットAPIと、独自カレンダーや曖昧日付の取り扱いについて解説します。

NodaTimeのフォーマットAPI

NodaTimeは.NET向けの高機能な日時処理ライブラリで、タイムゾーンやカレンダーの扱いに強みがあります。

標準のDateTimeDateTimeOffsetよりも正確で柔軟な日時管理が可能です。

NodaTimeの日時型とフォーマット

NodaTimeでは、LocalDate(日付のみ)、LocalTime(時刻のみ)、ZonedDateTime(タイムゾーン付き日時)などの型が用意されています。

これらは専用のフォーマッターを使って文字列に変換します。

using System;
using NodaTime;
using NodaTime.Text;
class Program
{
    static void Main()
    {
        // LocalDateの生成
        LocalDate date = new LocalDate(2025, 5, 7);
        // LocalTimeの生成
        LocalTime time = new LocalTime(8, 55, 31);
        // ZonedDateTimeの生成(Asia/Tokyoタイムゾーン)
        DateTimeZone tz = DateTimeZoneProviders.Tzdb["Asia/Tokyo"];
        ZonedDateTime zonedDateTime = date.At(time).InZoneLeniently(tz);
        // フォーマットパターンの作成
        var pattern = ZonedDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss z", DateTimeZoneProviders.Tzdb);
        // フォーマット実行
        string formatted = pattern.Format(zonedDateTime);
        Console.WriteLine(formatted);
    }
}
2025-05-07 08:55:31 JST

NodaTimeの特徴的なフォーマット機能

  • タイムゾーン名やオフセットの表示

zo<+HH:mm>などのトークンでタイムゾーン名やオフセットを柔軟に表示可能です。

  • カスタムパターンの自由度

標準の.NET書式指定子とは異なる独自のパターン文字列を使い、より詳細な制御ができます。

  • カルチャ非依存のフォーマット

InvariantCultureを使うことで、言語や地域に依存しない一貫したフォーマットが可能です。

  • パース機能も充実

フォーマットだけでなく、文字列から日時への変換(パース)も強力にサポートしています。

NodaTimeの導入方法

NodaTimeはNuGetパッケージとして提供されており、以下のコマンドでインストールできます。

dotnet add package NodaTime

独自カレンダーや曖昧日付の取扱い

標準のDateTimeDateTimeOffsetはグレゴリオ暦を前提としているため、独自のカレンダーや曖昧な日付(例:月のみ、年のみ、四半期など)を扱うのは難しい場合があります。

こうした特殊な要件に対応するために、外部ライブラリや独自実装を検討します。

独自カレンダーの例

  • 和暦の拡張

.NET標準のJapaneseCalendarは元号に対応していますが、独自の元号や歴史的なカレンダーを扱いたい場合は、カスタムカレンダークラスを実装するか、NodaTimeのカレンダー機能を利用します。

  • イスラム暦やヒジュラ暦

.NET標準にもHijriCalendarがありますが、より詳細な制御や変換が必要な場合は外部ライブラリを使うことがあります。

  • 農暦やその他の伝統的カレンダー

特殊なカレンダーは標準でサポートされていないため、独自実装や専門ライブラリの導入が必要です。

曖昧日付の取扱い

曖昧日付とは、日付の一部だけが判明している状態(例:2025年5月のみ、2025年第2四半期など)を指します。

標準の日時型では完全な日時を表す必要があるため、曖昧日付を表現するには以下のような方法があります。

  • 専用クラスの作成

年月だけを保持するYearMonthクラスや、四半期を表すクラスを独自に作成し、必要に応じて文字列化や比較を実装します。

  • NodaTimeのYearMonth

NodaTimeにはYearMonth型があり、年月だけを表現できます。

これを使うと曖昧日付の管理が容易です。

using System;
using NodaTime;
class Program
{
    static void Main()
    {
        YearMonth ym = new YearMonth(2025, 5);
        Console.WriteLine(ym); // 2025-05
        // 年月からLocalDateを作成(1日を仮定)
        LocalDate firstDay = ym.AtDay(1);
        Console.WriteLine(firstDay); // 2025-05-01
    }
}
2025-05
2025-05-01
  • 曖昧日付のフォーマット

曖昧日付用のフォーマットはカスタム実装が必要ですが、NodaTimeの型を使うと標準的な文字列化が可能です。

外部ライブラリを活用することで、標準の日時型では難しい高度な日時処理や特殊なカレンダー対応が実現できます。

特にNodaTimeは.NETコミュニティで広く使われており、信頼性と機能性の両面で優れています。

独自カレンダーや曖昧日付の要件がある場合は、こうしたライブラリの導入を検討するとよいでしょう。

テストでフォーマットを保証する

日時のフォーマットはユーザーインターフェースやログ、データ連携など多くの場面で重要な役割を果たします。

フォーマットの誤りは表示の乱れやデータ不整合につながるため、テストで正確に保証することが大切です。

ここでは単体テストの基本的な書き方と、カルチャ依存を排除する方法、さらにスナップショットテストの活用について解説します。

単体テストの書き方

日時フォーマットの単体テストでは、期待される文字列と実際にフォーマットされた文字列を比較して検証します。

テストフレームワークはxUnit、NUnit、MSTestなどがよく使われます。

期待文字列と実際文字列の比較

最も基本的な方法は、期待するフォーマット済み文字列を用意し、実際の出力と一致するかをアサートすることです。

using System;
using Xunit;
public class DateFormatTests
{
    [Fact]
    public void FormatDate_ReturnsExpectedString()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        string expected = "2025-05-07 08:55:31";
        string actual = dt.ToString("yyyy-MM-dd HH:mm:ss");
        Assert.Equal(expected, actual);
    }
}

このテストは、ToStringのフォーマット結果が期待通りであることを保証します。

フォーマット文字列の変更やコードの修正で意図しない変化があった場合に検出できます。

カルチャを固定したテスト方法

日時のフォーマットはカルチャに依存するため、テスト環境のカルチャが異なるとテストが失敗することがあります。

これを防ぐために、テスト内でカルチャを明示的に指定するか、スレッドのカルチャを固定します。

using System;
using System.Globalization;
using System.Threading;
using Xunit;
public class DateFormatCultureTests
{
    [Fact]
    public void FormatDate_WithInvariantCulture_ReturnsExpectedString()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        string expected = "2025-05-07 08:55:31";
        string actual = dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
        Assert.Equal(expected, actual);
    }
    [Fact]
    public void FormatDate_WithFixedThreadCulture_ReturnsExpectedString()
    {
        var originalCulture = Thread.CurrentThread.CurrentCulture;
        try
        {
            Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
            DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
            string expected = "2025-05-07 08:55:31";
            string actual = dt.ToString("F"); // 長い日時形式(InvariantCulture)
            Assert.Equal(expected, actual);
        }
        finally
        {
            Thread.CurrentThread.CurrentCulture = originalCulture;
        }
    }
}

このようにカルチャを固定することで、環境に依存しない安定したテストが可能になります。

スナップショットテストの活用

スナップショットテストは、出力結果をファイルやメモリに保存し、将来のテスト実行時に差分を検出する手法です。

日時フォーマットのテストにも応用でき、特に複雑なフォーマットや多言語対応のUIで効果的です。

スナップショットテストの例(xUnit + Verifyライブラリ)

using System;
using System.Threading.Tasks;
using VerifyXunit;
using Xunit;
[UsesVerify]
public class DateFormatSnapshotTests
{
    [Fact]
    public Task FormatDate_SnapshotTest()
    {
        DateTime dt = new DateTime(2025, 5, 7, 8, 55, 31);
        string formatted = dt.ToString("F", CultureInfo.InvariantCulture);
        return Verifier.Verify(formatted);
    }
}

このテストは初回実行時にformattedの内容をスナップショットファイルとして保存し、次回以降の実行で差分があればテストが失敗します。

フォーマットの変更を検知しやすく、複数のフォーマットパターンをまとめて管理するのに便利です。

スナップショットテストのメリット

  • 期待値の文字列をコードに直接書かずに済むため、可読性が向上する
  • フォーマットの微妙な変更を自動で検出できる
  • 多言語対応や複雑なフォーマットのテストに適している

日時フォーマットのテストは、単純な文字列比較だけでなく、カルチャ依存の排除やスナップショットテストの活用でより堅牢にできます。

これにより、フォーマットの意図しない変更を早期に発見し、品質を維持しやすくなります。

参考用クイックチャート

日時のフォーマットを行う際に役立つ、標準指定子とカスタムトークンの早見表をまとめました。

これらを手元に置いておくと、書式指定子をすばやく確認でき、効率的にフォーマットを作成できます。

標準指定子早見表

指定子説明例(2025年5月7日 08:55:31)備考
d短い日付2025/05/07カルチャ依存
D長い日付2025年5月7日水曜日カルチャ依存
t短い時刻8:55 午前カルチャ依存
T長い時刻8:55:31 午前カルチャ依存
f長い日付+短い時刻2025年5月7日 8:55カルチャ依存
F長い日付+長い時刻2025年5月7日水曜日 8:55:31カルチャ依存
g短い日付+短い時刻2025/5/7 8:55カルチャ依存
G短い日付+長い時刻2025/5/7 8:55:31カルチャ依存
M / m月日5月7日カルチャ依存
Y / y年月2025年5月カルチャ依存
sソート可能な日時(ISO 8601)2025-05-07T08:55:31タイムゾーンなし
uUTCのソート可能な日時2025-05-07 08:55:31ZUTC固定
r / RRFC 1123形式Wed, 07 May 2025 08:55:31 GMT英語固定、UTC
o / Oラウンドトリップ形式2025-05-07T08:55:31.0000000+09:00ISO 8601、タイムゾーン含む

カスタムトークン早見表

トークン説明例(2025年5月7日 08:55:31)備考
yyyy4桁の年2025
yy2桁の年25
MM2桁の月(01~12)05
MMM月の英語略称Mayカルチャ依存
MMMM月の英語フル名Mayカルチャ依存
dd2桁の日(01~31)07
d1桁または2桁の日7
ddd曜日の略称カルチャ依存
dddd曜日のフル名水曜日カルチャ依存
HH24時間制の時(00~23)08
hh12時間制の時(01~12)08
mm分(00~59)55
ss秒(00~59)31
fffミリ秒(000~999)000
ttAM/PM表示午前カルチャ依存
zzzタイムゾーンオフセット+09:00
‘…’リテラル文字yyyy’年’MM’月’dd’日シングルクォーテーションで囲む
“…”リテラル文字yyyy”年”MM”月”dd”日”ダブルクォーテーションで囲む
\エスケープ文字yyyy-MM-dd\THH:mm:ssバックスラッシュで次の文字をリテラル化

このクイックチャートを活用して、日時フォーマットの指定子をすばやく確認し、効率的にフォーマット文字列を作成してください。

まとめ

この記事では、C#の日時フォーマットに関する基本から応用まで幅広く解説しました。

標準書式指定子やカスタム書式指定子の使い方、カルチャ依存の注意点、.NET 6以降の新型型DateOnlyTimeOnlyの活用法、さらに外部ライブラリNodaTimeの利用やカスタムIFormatProviderによる拡張方法も紹介しています。

実務で役立つフォーマットパターンやテスト手法も網羅し、日時フォーマットの正確性と柔軟性を高めるための知識が得られます。

関連記事

Back to top button
目次へ