ここでは、書式プロバイダ(IFormatProvider)、カスタム書式プロバイダ(ICustomFormatter)の役割と、その実装方法ついて見ていきます。

§1 書式プロバイダ (IFormatProvider)

書式プロバイダ(IFormatProvider)は、 D, G などの書式指定子で指定される書式から、それらに対応する形式の文字列にするためのメソッドを提供するインターフェイスです。 書式プロバイダはToString等の書式化を行うメソッドから参照されます。 明示的に書式プロバイダが指定された場合はそれが用いられ、特に指定されない場合はデフォルトとして現在のスレッドのカルチャ(後述)が書式プロバイダとして使用されます。

§1.1 書式プロバイダとスレッドのカルチャ

ToString等のメソッドでは、引数にIFormatProviderをとるオーバーロードが用意されています。 これらのメソッドでIFormatProviderを指定した場合はそれが書式プロバイダとして機能し、書式化の際に使用されます。 書式プロバイダを指定しなかった場合は、現在のスレッドのカルチャ(Thread.CurrentThread.CurrentCulture)が提供する書式を使ってフォーマットされます。

CultureInfoなどIFormatProviderを実装するインスタンスを指定してToString等を呼び出すか、ToString等を呼び出す前にThread.CurrentThread.CurrentCultureにCultureInfoを設定しておくことで、特定のカルチャ(ロケール)での書式や独自に定義された書式で値をフォーマットすることが出来ます。

スレッドのカルチャおよび書式プロバイダとして指定したカルチャによるフォーマット結果の違い
using System;
using System.Threading;
using System.Globalization;

class Sample {
  static void Main()
  {
    DateTime dt = new DateTime(2010, 9, 14, 1, 2, 3, 456);

    // IFormatProviderを指定しない場合
    // (現在のスレッドのカルチャ'ja-JP'での書式でフォーマットされる)
    Console.WriteLine("{0} {1:U}", Thread.CurrentThread.CurrentCulture, dt);

    // Thread.CurrentThread.CurrentCultureに'en-US'のCultureInfoを指定した場合
    // (現在のスレッドのカルチャ'en-US'での書式でフォーマットされる)
    Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-US");

    Console.WriteLine("{0} {1:U}", Thread.CurrentThread.CurrentCulture, dt);

    // Thread.CurrentThread.CurrentCultureに'es-ES'のCultureInfoを指定した場合
    // (現在のスレッドのカルチャ'es-ES'での書式でフォーマットされる)
    Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("es-ES");

    Console.WriteLine("{0} {1:U}", Thread.CurrentThread.CurrentCulture, dt);

    // ToStringメソッドでIFormatProviderを指定した場合
    // (引数で指定したIFormatProvider=カルチャ'ja-JP'での書式でフォーマットされる)
    Console.WriteLine("{0} {1}", Thread.CurrentThread.CurrentCulture, dt.ToString("U", CultureInfo.GetCultureInfo("ja-JP")));

    // String.FormatメソッドでIFormatProviderを指定した場合
    // (上と同様、引数で指定したIFormatProvider=カルチャ'ja-JP'での書式でフォーマットされる)
    Console.WriteLine(String.Format(CultureInfo.GetCultureInfo("ja-JP"), "{0} {1:U}", Thread.CurrentThread.CurrentCulture, dt));
  }
}
実行結果
ja-JP 2010年9月13日 16:02:03
en-US Monday, September 13, 2010 4:02:03 PM
es-ES lunes, 13 de septiembre de 2010 16:02:03
es-ES 2010年9月13日 16:02:03
es-ES 2010年9月13日 16:02:03

標準の書式指定子に対応する書式や、書式化される際に使用される文字列は、CultureInfoに設定されるNumberFormatInfoDateTimeFormatInfoに設定されている値を元にして組み立てられます。 これらのクラスでは、月名・曜日名の表記や、桁区切り・小数点・通貨単位などの記号が設定されていて、これらの値を変更する事で書式をカスタマイズすることも出来ます。

なお、'ja'や'en'など言語コードのみを指定して作成したCultureInfo(ニュートラルカルチャのCultureInfo)には書式が設定されないため、書式プロバイダとしては機能しません。 ニュートラルカルチャのCultureInfoを指定するとNotSupportedExceptionがスローされます。

スレッドとカルチャについてはカルチャの基本とカルチャ情報、カルチャと書式の関係やNumberFormatInfoに設定される値などの詳細についてはカルチャと書式・テキスト処理・暦で詳しく解説しています。

§1.2 カスタム書式プロバイダ (ICustomFormatter)

IFormatProviderインターフェイスICustomFormatterインターフェイスを実装することで、書式指定子とそれに対応する書式を独自に定義することができます。 ICustomFormatterでは書式指定子から適切な書式の文字列へと変換する処理を実装し、IFormatProviderでは変換を行うフォーマッタを取得するための処理を実装します。

書式化の際にカスタム書式プロバイダがどのように用いられるかを述べると次のようになります。 まず、数値型やDateTime型を書式化する場合、IFormatProviderからNumberFormatInfoやDateTimeFormatInfoを取得しようとします。 それ以外の型の場合や、IFormatProviderからNumberFormatInfoやDateTimeFormatInfoを取得出来なかった場合は、代わりにICustomFormatterを取得しようとします。 この時に適切なICustomFormatterを返すことで、独自に定義した書式で文字列化されることになります。

§1.2.1 ICustomFormatterの実装例

以下は、書式指定子に応じて文字列を大文字化・小文字化・タイトルケース化するカスタム書式プロバイダTextFormatProviderを実装する例です。 カスタム書式プロバイダTextFormatProviderでは、次のように定義された書式をサポートします。

サポートする書式
書式指定子 書式 フォーマット例
入力文字列 フォーマット結果
U, u 与えられた文字列を大文字化する "Hello world" "HELLO WORLD"
L, l 与えられた文字列を小文字化する "hello world"
T, t 与えられた文字列をタイトルケース化する "Hello World"

この例で実装しているICustomFormatter.Formatメソッドの動作は次のとおりです。

  • 引数が文字列の場合
    • 書式指定子が U, u の場合は、引数で指定された文字列を大文字化して返す
    • 書式指定子が L, l の場合は、引数で指定された文字列を小文字化して返す
    • 書式指定子が T, t の場合は、引数で指定された文字列をタイトルケース化して返す
    • 書式指定子が指定されていない場合は、引数で指定された文字列をそのまま返す
    • 上記以外の書式指定子の場合、FormatExceptionをスローする
  • 引数が文字列以外の場合
    • 引数がIFormattableを実装している場合は、IFormattable.ToStringの結果を返す
    • 引数がIFormattableを実装していない場合は、Object.ToStringの結果を返す

なお、この例では大文字化・小文字化・タイトルケース化にTextInfoクラスToUpperメソッドToLowerメソッドToTitleCaseメソッドを使っています。 TextInfoクラスについてはカルチャと書式・テキスト処理・暦を参照してください。 また、この例で触れているIFormattableインターフェイスについては後ほど解説します。

ICustomFormatterインターフェイスを使ってカスタム書式プロバイダを実装する例
using System;
using System.Globalization;

class TextFormatProvider : IFormatProvider, ICustomFormatter {
  // IFormatProvider.GetFormatの明示的な実装
  object IFormatProvider.GetFormat(Type formatType)
  {
    if (formatType == typeof(ICustomFormatter))
      // IFormatProviderにICustomFormatter型を要求された場合は自分自身を返す
      return this;
    else
      // それ以外の場合はnullを返す
      return null;
  }

  // ICustomFormatter.Formatの実装
  public string Format(string format, object arg, IFormatProvider formatProvider)
  {
    string argString = arg as string;

    if (argString != null) {
      // 引数がstring型の場合、指定された書式に合わせて文字列化する
      switch (format) {
        case "u":
        case "U": // 書式指定子が"U"または"u"の時は、すべて大文字にする
          return CultureInfo.CurrentCulture.TextInfo.ToUpper(argString);

        case "l":
        case "L": // 書式指定子が"L"または"l"の時は、すべて小文字にする
          return CultureInfo.CurrentCulture.TextInfo.ToLower(argString);

        case "t":
        case "T": // 書式指定子が"T"または"t"の時は、タイトルケースにする
          return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(argString);

        default:
          if (string.IsNullOrEmpty(format))
            // 書式指定子が指定されていない場合は、そのままにする
            return argString;
          else
            // それ以外の書式指定子の場合は、FormatExceptionをスローする
            throw new FormatException(string.Format("'{0}'は不正な書式指定子です", format));
      }
    }

    // 引数がString型でない場合
    if (arg is IFormattable)
      // 引数がIFormattableを実装している場合は、IFormattable.ToStringメソッドで文字列化する
      return ((IFormattable)arg).ToString(format, formatProvider);
    else
      // 引数がIFormattableを実装していない場合は、Object.ToStringメソッドで文字列化する
      return arg.ToString();
  }
}

class Sample {
  static void Main()
  {
    IFormatProvider fp = new TextFormatProvider();

    // 文字列を書式化
    Console.WriteLine(string.Format(fp, "{0} {0:U} {0:u} {0:L} {0:l} {0:T} {0:t}", "HoGe"));
    Console.WriteLine();

    string text = "The quick brown fox jumps over the lazy dog";

    Console.WriteLine(string.Format(fp, "{0}", text));
    Console.WriteLine(string.Format(fp, "{0:U}", text));
    Console.WriteLine(string.Format(fp, "{0:L}", text));
    Console.WriteLine(string.Format(fp, "{0:T}", text));
    Console.WriteLine();

    // int型の値を書式化
    int intValue = 42;

    Console.WriteLine(string.Format(fp, "{0:D4} {0:F2}", intValue));
    Console.WriteLine(intValue.ToString("X8", fp));
    Console.WriteLine();

    // DateTime型の値を書式化
    DateTime dateTimeValue = new DateTime(2010, 9, 14, 1, 2, 3);

    Console.WriteLine(string.Format(fp, "{0:u} {0:s}", dateTimeValue));
    Console.WriteLine(dateTimeValue.ToString("r", fp));
    Console.WriteLine();

    // 不正な書式を指定
    try {
      Console.WriteLine(string.Format(fp, "{0:U} {0:X}", "HoGe"));
    }
    catch (FormatException ex) {
      Console.Error.WriteLine(ex.Message);
    }
  }
}
実行結果
HoGe HOGE HOGE hoge hoge Hoge Hoge

The quick brown fox jumps over the lazy dog
THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG
the quick brown fox jumps over the lazy dog
The Quick Brown Fox Jumps Over The Lazy Dog

0042 42.00
0000002A

2010-09-14 01:02:03Z 2010-09-14T01:02:03
Tue, 14 Sep 2010 01:02:03 GMT

'X'は不正な書式指定子です


§2 書式を指定した文字列化のサポート (IFormattable)

クラスや構造体にIFormattableインターフェイスを実装すると、独自に定義した書式で文字列化するToStringメソッドを用意することができます。 int, double, DateTime等の基本型はIFormattableインターフェイスを実装していて、これにより書式を指定して文字列化できるようになっています。 ICustomFormatterでは書式化の実装を書式化したい型とは別のクラスで実装するのに対し、IFormattableでは書式化の実装を書式化したい型自体に実装することができます。

§2.1 IFormattableの実装例

以下は、複素数型Complexを用意し、IFormattableで書式化処理を実装する例です。 この例では次の書式指定子をサポートします。

サポートする書式
書式指定子 書式 フォーマット例
値と書式指定子 フォーマット結果
Gn 精度nのガウス平面座標表示
"(a, b)"の形式
(new Complex(4.0, 3.0)).ToString("G") "(4.00, 3.00)"
(new Complex(4.0, 3.0)).ToString("G4") "(4.0000, 3.0000)"
An 精度nの絶対値表示
|z|の値の形式
(new Complex(4.0, 3.0)).ToString("A") "5.00"
(new Complex(-4.0, -3.0)).ToString("A4") "5.0000"
Cn 精度nの直交座標表示
"a±bi"の形式
(実部または虚部が0の場合は表示しない)
(new Complex(4.0, 3.0)).ToString("C1") "4.0+3.0i"
(new Complex(0.0, -1.0)).ToString("C1") "-1.0i"
Pn 精度nの極座標表示
"rθ"の形式
(new Complex(4.0, 3.0)).ToString("P1") "5.0∠0.2π"
(new Complex(1.414, 1.414)).ToString("P3") "2.000∠0.250π"

なお、この例では書式指定子で精度nが省略された場合は書式指定子 F のデフォルトの精度で文字列化します。 また、double型の値を書式指定子 F で文字列化する際に、引数で渡されたIFormatProviderを使って書式化するようにしています。 そのため、CultureInfoが渡された場合はそのカルチャでの書式に従って文字列化されます。

IFormattableインターフェイスを使って型に書式化機能を実装する例
using System;
using System.Globalization;

struct Complex : IFormattable {
  public double Real; // 実部
  public double Imaginary; // 虚部

  public Complex(double real, double imaginary)
    : this()
  {
    this.Real = real;
    this.Imaginary = imaginary;
  }

  // Object.ToStringのオーバーライド
  public override string ToString()
  {
    // 書式と書式プロバイダを指定せずにToStringを呼ぶ
    return ToString(null, null);
  }

  public string ToString(string format)
  {
    // 書式プロバイダを指定せずにToStringを呼ぶ
    return ToString(format, null);
  }

  // IFormattable.ToString(string, IFormatProvider)の実装
  public string ToString(string format, IFormatProvider formatProvider)
  {
    if (string.IsNullOrEmpty(format)) format = "G"; // 指定されていない場合は"G"の書式を使用

    char f = format[0]; // 書式指定文字列の1文字目で書式の種類を分ける
    string acc = format.Substring(1); // 書式指定文字列の2文字目以降を精度として扱う

    switch (f) {
      case 'G': // ガウス平面座標表示
        return string.Format(formatProvider, "({0:F" + acc + "}, {1:F" + acc + "})", Real, Imaginary);

      case 'A': // 絶対値表示
        return Math.Sqrt(Real * Real + Imaginary * Imaginary).ToString("F" + acc, formatProvider);

      case 'C': // 直交形式表示
      {
        // 実部の値が0の場合は表示しない
        string realPart = (Real == 0.0) ? null : Real.ToString("F" + acc, formatProvider);
        string imaginaryPart;
        string sign;

        if (Imaginary == 0.0) {
          // 虚部の値が0の場合は表示しない
          imaginaryPart = null;
          sign = null;
        }
        else if (0.0 < Imaginary) {
          imaginaryPart = Imaginary.ToString("F" + acc, formatProvider) + "i";
          sign = (realPart == null) ? null : "+"; // 実部を表示する場合のみ符号を付ける
        }
        else /*if (Imaginary < 0.0)*/ {
          imaginaryPart = (-Imaginary).ToString("F" + acc, formatProvider) + "i";
          sign = "-";
        }

        return string.Concat(realPart, sign, imaginaryPart);
      }

      case 'P': // 極形式表示
      {
        double r = Math.Sqrt(Real * Real + Imaginary * Imaginary);
        double t = Math.Atan2(Imaginary, Real) / Math.PI;

        return string.Format(formatProvider, "{0:F" + acc + "}∠{1:F" + acc + "}π", r, t);
      }

      default:
        throw new FormatException(string.Format("'{0}'は不正な書式指定子です", format));
    }
  }
}

class Sample {
  static void Main()
  {
    Console.WriteLine("{0}, {0:G}, {0:A}, {0:C1}, {0:P6}", (new Complex(4.0, 3.0)));

    Console.WriteLine();
    Console.WriteLine((new Complex( 3.000,  4.000)).ToString());
    Console.WriteLine((new Complex( 1.414,  1.414)).ToString());
    Console.WriteLine((new Complex( 3.000,  4.000)).ToString("G", CultureInfo.GetCultureInfo("ja-JP")));
    Console.WriteLine((new Complex(-1.414,  1.414)).ToString("G4", CultureInfo.GetCultureInfo("en-US")));
    Console.WriteLine((new Complex( 0.000,  1.000)).ToString("G12", CultureInfo.GetCultureInfo("fr-FR")));

    Console.WriteLine();
    Console.WriteLine((new Complex( 3.000,  4.000)).ToString("A"));
    Console.WriteLine((new Complex(-4.000, -3.000)).ToString("A4", CultureInfo.GetCultureInfo("ja-JP")));
    Console.WriteLine((new Complex( 1.000, -1.000)).ToString("A1", CultureInfo.GetCultureInfo("en-US")));
    Console.WriteLine((new Complex( 2.000,  0.000)).ToString("A1", CultureInfo.GetCultureInfo("fr-FR")));

    Console.WriteLine();
    Console.WriteLine((new Complex( 3.000,  4.000)).ToString("C"));
    Console.WriteLine((new Complex(-0.707,  0.707)).ToString("C2"));
    Console.WriteLine((new Complex( 0.707, -0.707)).ToString("C2"));
    Console.WriteLine((new Complex( 1.000,  0.000)).ToString("C1"));
    Console.WriteLine((new Complex(-1.000,  0.000)).ToString("C1", CultureInfo.GetCultureInfo("ja-JP")));
    Console.WriteLine((new Complex( 0.000,  1.000)).ToString("C1", CultureInfo.GetCultureInfo("en-US")));
    Console.WriteLine((new Complex( 0.000, -1.000)).ToString("C1", CultureInfo.GetCultureInfo("fr-FR")));
    Console.WriteLine((new Complex( 1.000,  1.000)).ToString("C1"));
    Console.WriteLine((new Complex( 1.000, -1.000)).ToString("C1"));

    Console.WriteLine();
    Console.WriteLine((new Complex( 1.000,  0.000)).ToString("P3"));
    Console.WriteLine((new Complex( 1.414,  1.414)).ToString("P3"));
    Console.WriteLine((new Complex( 0.000,  1.000)).ToString("P3", CultureInfo.GetCultureInfo("ja-JP")));
    Console.WriteLine((new Complex(-1.000,  0.000)).ToString("P3", CultureInfo.GetCultureInfo("en-US")));
    Console.WriteLine((new Complex( 0.000, -1.000)).ToString("P3", CultureInfo.GetCultureInfo("fr-FR")));
    Console.WriteLine((new Complex( 0.000,  0.000)).ToString("P3"));
    Console.WriteLine((new Complex(-3.000,  4.000)).ToString("P3"));
  }
}
実行結果
(4.00, 3.00), (4.00, 3.00), 5.00, 4.0+3.0i, 5.000000∠0.204833π

(3.00, 4.00)
(1.41, 1.41)
(3.00, 4.00)
(-1.4140, 1.4140)
(0,000000000000, 1,000000000000)

5.00
5.0000
1.4
2,0

3.00+4.00i
-0.71+0.71i
0.71-0.71i
1.0
-1.0
1.0i
-1,0i
1.0+1.0i
1.0-1.0i

1.000∠0.000π
2.000∠0.250π
1.000∠0.500π
1.000∠1.000π
1,000∠-0,500π
0.000∠0.000π
5.000∠0.705π

.NET Framework 4より導入されているSystem.Numerics.Complex構造体では、このような複数の書式はサポートされていません。

System.Numerics.Complex構造体については、複素数型で詳しく解説しています。