グローバリゼーション

グローバリゼーションとは、さまざまな文化圏 (カルチャ) のユーザー向けに、ローカライズされたインターフェイスと、その地域に合ったデータをサポートするような、国際対応アプリの設計と開発をいいます。 設計フェーズに着手する前に、アプリでサポートするカルチャを決定してください。 アプリは既定値として 1 つのカルチャまたは地域を対象としますが、別のカルチャまたは地域のユーザーに簡単に拡張できるようにアプリを設計および作成できます。

開発者には、自分のカルチャによって形成されるユーザー インターフェイスとデータに関する前提があります。 たとえば、米国の英語圏の開発者は、日付と時刻のデータを形式 MM/dd/yyyy hh:mm:ss の文字列としてシリアル化することは、十分理にかなっていると考えます。 ただし、別のカルチャのシステムでその文字列を逆シリアル化すると、FormatException 例外がスローされるか、正しくないデータが生成される可能性があります。 グローバリゼーションにより、このようなカルチャ固有の前提を識別し、それがアプリの設計またはコードに影響しないようにすることができます。

この記事では、グローバライズされたアプリで文字列、日付と時刻の値、および数値を処理するときに考慮する必要がある重要な問題および使用できるベスト プラクティスについて説明します。

文字列

カルチャまたは地域ごとに異なる文字と文字セットを使用し、異なる方法で並べ替える可能性があるため、文字と文字列の処理は、グローバリゼーションで主な焦点となります。 このセクションでは、グローバライズされたアプリで文字列を使用するための推奨事項について説明します。

Unicode を内部で使用する

既定では、.NET は Unicode 文字列を使用します。 Unicode 文字列は、ゼロ個以上の Char オブジェクトで構成され、それぞれが UTF-16 コード単位を表します。 世界中で使用されているすべての文字セットのほとんどすべての文字に対して、Unicode 表現があります。

また、Windows オペレーティング システムを含む、多くのアプリケーションとオペレーティング システムでは、文字セットを表すためにコード ページを使用することもできます。 コード ページには、通常、0x00 から 0x7F までの標準 ASCII 値が含まれ、0x80 から 0xFF までの残りの値に他の文字をマップします。 0x80 から 0xFF までの値の解釈は特定のコード ページによって異なります。 このため、可能な場合は、グローバライズされたアプリでコード ページを使用しないようにします。

次の例では、システムの既定のコード ページとデータが保存されたコード ページとが異なる場合にコード ページのデータを解釈する危険性を示しています (このシナリオをシミュレートするため、例では、異なるコード ページを明示的に指定します)。最初に、ギリシャ文字の大文字で構成される配列を定義します。 コード ページ 737 (MS-DOS ギリシャ語とも呼ばれます) を使用してそれらをバイト配列にエンコードし、バイト配列をファイルに保存します。 ファイルを取得し、そのバイト配列をコード ページ 737 を使用してデコードした場合、元の文字が復元されます。 ただし、ファイルを取得して、そのバイト配列をコード ページ 1252 (または Windows-1252。ラテン語アルファベットの文字を表します) を使用してデコードした場合、元の文字は失われます。

using System;
using System.IO;
using System.Text;

public class Example
{
    public static void CodePages()
    {
        // Represent Greek uppercase characters in code page 737.
        char[] greekChars =
        {
            'Α', 'Β', 'Γ', 'Δ', 'Ε', 'Ζ', 'Η', 'Θ',
            'Ι', 'Κ', 'Λ', 'Μ', 'Ν', 'Ξ', 'Ο', 'Π',
            'Ρ', 'Σ', 'Τ', 'Υ', 'Φ', 'Χ', 'Ψ', 'Ω'
        };

        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

        Encoding cp737 = Encoding.GetEncoding(737);
        int nBytes = cp737.GetByteCount(greekChars);
        byte[] bytes737 = new byte[nBytes];
        bytes737 = cp737.GetBytes(greekChars);
        // Write the bytes to a file.
        FileStream fs = new FileStream(@".\\CodePageBytes.dat", FileMode.Create);
        fs.Write(bytes737, 0, bytes737.Length);
        fs.Close();

        // Retrieve the byte data from the file.
        fs = new FileStream(@".\\CodePageBytes.dat", FileMode.Open);
        byte[] bytes1 = new byte[fs.Length];
        fs.Read(bytes1, 0, (int)fs.Length);
        fs.Close();

        // Restore the data on a system whose code page is 737.
        string data = cp737.GetString(bytes1);
        Console.WriteLine(data);
        Console.WriteLine();

        // Restore the data on a system whose code page is 1252.
        Encoding cp1252 = Encoding.GetEncoding(1252);
        data = cp1252.GetString(bytes1);
        Console.WriteLine(data);
    }
}

// The example displays the following output:
//       ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ
//       €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’""•–—

Imports System.IO
Imports System.Text

Module Example
    Public Sub CodePages()
        ' Represent Greek uppercase characters in code page 737.
        Dim greekChars() As Char = {"Α"c, "Β"c, "Γ"c, "Δ"c, "Ε"c, "Ζ"c, "Η"c, "Θ"c,
                                     "Ι"c, "Κ"c, "Λ"c, "Μ"c, "Ν"c, "Ξ"c, "Ο"c, "Π"c,
                                     "Ρ"c, "Σ"c, "Τ"c, "Υ"c, "Φ"c, "Χ"c, "Ψ"c, "Ω"c}

        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)

        Dim cp737 As Encoding = Encoding.GetEncoding(737)
        Dim nBytes As Integer = CInt(cp737.GetByteCount(greekChars))
        Dim bytes737(nBytes - 1) As Byte
        bytes737 = cp737.GetBytes(greekChars)
        ' Write the bytes to a file.
        Dim fs As New FileStream(".\CodePageBytes.dat", FileMode.Create)
        fs.Write(bytes737, 0, bytes737.Length)
        fs.Close()

        ' Retrieve the byte data from the file.
        fs = New FileStream(".\CodePageBytes.dat", FileMode.Open)
        Dim bytes1(CInt(fs.Length - 1)) As Byte
        fs.Read(bytes1, 0, CInt(fs.Length))
        fs.Close()

        ' Restore the data on a system whose code page is 737.
        Dim data As String = cp737.GetString(bytes1)
        Console.WriteLine(data)
        Console.WriteLine()

        ' Restore the data on a system whose code page is 1252.
        Dim cp1252 As Encoding = Encoding.GetEncoding(1252)
        data = cp1252.GetString(bytes1)
        Console.WriteLine(data)
    End Sub
End Module
' The example displays the following output:
'       ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ
'       €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’""•–—

Unicode を使用することで、同じコード単位を必ず同じ文字にマップでき、同じ文字を必ず同じバイト配列にマップできます。

リソース ファイルを使用する

単一のカルチャまたは地域を対象とするアプリを開発する場合でも、ユーザー インターフェイスに表示される文字列などのリソースを格納するためにリソース ファイルを使用する必要があります。 リソースを直接コードに追加しないでください。 リソース ファイルの使用には、次の利点があります。

  • すべての文字列が単一の場所に存在します。 ソース コード全体を検索して、特定の言語またはカルチャに合わせて変更する文字列を特定する必要はありません。
  • 文字列を複製する必要がありません。 リソース ファイルを使用しない開発者は、多くの場合、複数のソース コード ファイルで同じ文字列を定義します。 この重複により、文字列を変更するときに 1 つ以上のインスタンスを見落とす可能性が高くなります。
  • イメージ、バイナリ データなどの文字列以外のリソースを個別のスタンドアロン ファイルに格納するのではなく、リソース ファイルに格納できるので、それらが簡単に取得できます。

ローカライズされたアプリを作成する場合、リソース ファイルを使用することには特に利点があります。 サテライト アセンブリにリソースを配置すると CultureInfo.CurrentUICulture プロパティで定義されているユーザーの現在の UI カルチャに基づいて、共通言語ランタイムが自動的にカルチャに応じたリソースを選択します。 適切なカルチャ固有のリソースを提供し、ResourceManager オブジェクトを正しくインスタンス化するか、厳密に型指定されたリソース クラスを使用すると、ランタイムは適切なリソースの取得の詳細を処理します。

リソース ファイルの作成の詳細については、リソース ファイルの作成に関する記事を参照してください。 サテライト アセンブリの作成と展開の詳細については、サテライト アセンブリの作成リソースのパッケージ化と展開に関するページを参照してください。

文字列の検索と比較を行う

文字列は、できるだけ全体を 1 つのまとまりとして扱い、個々の文字の連続として処理しない必要があります。 これは、部分文字列を並べ替えたり、検索したりするときに、組み合わせ文字の解析に関する問題を防ぐうえで特に重要です。

ヒント

StringInfo クラスを使用して、文字列の個別の文字ではなく、テキスト要素を操作できます。

文字列の検索と比較でよくある間違いは、それぞれが Char オブジェクトによって表される文字のコレクションとして文字列を処理することです。 実際に、1 つの文字が 1 つ、2 つ、またはそれ以上の Char オブジェクトによって形成される場合があります。 このような文字は、アルファベットが、Unicode 基本ラテン文字の範囲 (U+0021 ~ U+007E) 外にある文字で構成されるカルチャの文字列に最もよくみられます。 次の例では、文字列で LATIN CAPITAL LETTER A WITH GRAVE 文字 (U+00C0) のインデックスを検索します。 ただし、この文字は、1 つのコード単位 (U+00C0) または複合文字 (2 つのコード単位:U+0041 および U+0300)。 この場合、この文字は、文字列インスタンスで 2 つの Char オブジェクト (U+0041 と U+0300) によって表されます。 このコード例では、String.IndexOf(Char) オーバーロードと String.IndexOf(String) オーバーロードを呼び出して、文字列インスタンスでのこの文字の位置を検索しますが、この 2 つは異なる結果を返します。 最初のメソッド呼び出しでは Char 引数を指定しているので、序数に基づく比較が実行され、一致を見つけることができません。 2 番目の呼び出しでは String 引数を指定しているので、カルチャに依存した比較が実行され、一致が見つかります。

using System;
using System.Globalization;
using System.Threading;

public class Example17
{
    public static void Main17()
    {
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("pl-PL");
        string composite = "\u0041\u0300";
        Console.WriteLine("Comparing using Char:   {0}", composite.IndexOf('\u00C0'));
        Console.WriteLine("Comparing using String: {0}", composite.IndexOf("\u00C0"));
    }
}

// The example displays the following output:
//       Comparing using Char:   -1
//       Comparing using String: 0
Imports System.Globalization
Imports System.Threading

Module Example17
    Public Sub Main17()
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("pl-PL")
        Dim composite As String = ChrW(&H41) + ChrW(&H300)
        Console.WriteLine("Comparing using Char:   {0}", composite.IndexOf(ChrW(&HC0)))
        Console.WriteLine("Comparing using String: {0}", composite.IndexOf(ChrW(&HC0).ToString()))
    End Sub
End Module
' The example displays the following output:
'       Comparing using Char:   -1
'       Comparing using String: 0

StringComparison メソッド、String.IndexOf(String, StringComparison) メソッドなど String.LastIndexOf(String, StringComparison) パラメーターを含むオーバーロードを呼び出して、この例のあいまいさ (異なる結果を返すメソッドの 2 つの類似オーバーロードの呼び出し) の一部を回避できます。

ただし、検索が常にカルチャに依存しているとは限りません。 検索の目的がセキュリティ上の決定をするか、またはリソースへのアクセスを許可または拒否することである場合、次のセクションで説明するように序数に基づく比較を実行する必要があります。

文字列の等価性をテストする

2 つの文字列が並べ替え順序で並ぶ方法を確認するのではなく、2 つの文字列の等価性をテストする場合は、String.CompareCompareInfo.Compare などの文字列比較メソッドの代わりに String.Equals メソッドを使用します。

等価性の比較は、通常、条件付きでリソースにアクセスするために実行します。 たとえば、パスワードを確認したり、ファイルがあることを確認したりするために等価性の比較を実行する場合があります。 このような非言語的な比較は、カルチャに依存するのではなく、常に序数に基づいて実行する必要があります。 一般に、パスワードなどの文字列の場合は String.Equals(String, StringComparison) の値を使用して、ファイル名、URI などの文字列の場合は String.Equals(String, String, StringComparison) の値を使用して、インスタンス StringComparison.Ordinal メソッドまたは静的 StringComparison.OrdinalIgnoreCase メソッドを呼び出す必要があります。

等価性の比較では、String.Equals メソッドを呼び出すのではなく、検索したり、部分文字列を比較したりすることがあります。 場合によっては、部分文字列検索を使用して、その部分文字列が別の文字列と等しいかどうかを確認できます。 この比較の目的が非言語的である場合、検索はカルチャに依存するのではなく、序数に基づいて実行する必要があります。

次の例は、非言語的なデータに対してカルチャに依存した検索を実行することの危険性を示しています。 AccessesFileSystem メソッドは、部分文字列 "FILE" で始まる URI のファイル システムのアクセスを禁止するように設計されています。 これを実行するため、カルチャに依存し、大文字と小文字を区別しない比較を実行して、URI の先頭と文字列 "FILE" を比較します。 ファイル システムにアクセスする URI は "FILE:" または "file:" で始まる可能性があるため、"i" (U+0069) は常に "I" (U+0049) と等価の小文字表現であるという暗黙の前提があります。 ただし、トルコ語およびアゼルバイジャン語には、"i" の大文字として "İ" (U+0130) があります。 このような相違があるため、カルチャに依存した比較を使用すると、ファイル システムのアクセスを禁止する必要がある場合でもそのアクセスが許可されます。

using System;
using System.Globalization;
using System.Threading;

public class Example10
{
    public static void Main10()
    {
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("tr-TR");
        string uri = @"file:\\c:\users\username\Documents\bio.txt";
        if (!AccessesFileSystem(uri))
            // Permit access to resource specified by URI
            Console.WriteLine("Access is allowed.");
        else
            // Prohibit access.
            Console.WriteLine("Access is not allowed.");
    }

    private static bool AccessesFileSystem(string uri)
    {
        return uri.StartsWith("FILE", true, CultureInfo.CurrentCulture);
    }
}

// The example displays the following output:
//         Access is allowed.
Imports System.Globalization
Imports System.Threading

Module Example10
    Public Sub Main10()
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("tr-TR")
        Dim uri As String = "file:\\c:\users\username\Documents\bio.txt"
        If Not AccessesFileSystem(uri) Then
            ' Permit access to resource specified by URI
            Console.WriteLine("Access is allowed.")
        Else
            ' Prohibit access.
            Console.WriteLine("Access is not allowed.")
        End If
    End Sub

    Private Function AccessesFileSystem(uri As String) As Boolean
        Return uri.StartsWith("FILE", True, CultureInfo.CurrentCulture)
    End Function
End Module
' The example displays the following output:
'       Access is allowed.

次の例に示すように、大文字と小文字を無視する序数に基づく比較を実行して、この問題を回避できます。

using System;
using System.Globalization;
using System.Threading;

public class Example11
{
    public static void Main11()
    {
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("tr-TR");
        string uri = @"file:\\c:\users\username\Documents\bio.txt";
        if (!AccessesFileSystem(uri))
            // Permit access to resource specified by URI
            Console.WriteLine("Access is allowed.");
        else
            // Prohibit access.
            Console.WriteLine("Access is not allowed.");
    }

    private static bool AccessesFileSystem(string uri)
    {
        return uri.StartsWith("FILE", StringComparison.OrdinalIgnoreCase);
    }
}

// The example displays the following output:
//         Access is not allowed.
Imports System.Globalization
Imports System.Threading

Module Example11
    Public Sub Main11()
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("tr-TR")
        Dim uri As String = "file:\\c:\users\username\Documents\bio.txt"
        If Not AccessesFileSystem(uri) Then
            ' Permit access to resource specified by URI
            Console.WriteLine("Access is allowed.")
        Else
            ' Prohibit access.
            Console.WriteLine("Access is not allowed.")
        End If
    End Sub

    Private Function AccessesFileSystem(uri As String) As Boolean
        Return uri.StartsWith("FILE", StringComparison.OrdinalIgnoreCase)
    End Function
End Module
' The example displays the following output:
'       Access is not allowed.

文字列の順序と並べ替え

通常、ユーザー インターフェイスの表示順序が指定された文字列は、カルチャに基づいて並べ替える必要があります。 ほとんどの場合、このような文字列比較は、文字列を並べ替える Array.SortList<T>.Sort などのメソッドを呼び出すと、.NET によって暗黙的に処理されます。 既定では、文字列は、現在のカルチャの並べ替え規則を使用して並べ替えられます。 次の例では、文字列の配列を英語 (米国) カルチャとスウェーデン語 (スウェーデン) カルチャの規則を使用して並べ替えたときの違いを示しています。

using System;
using System.Globalization;
using System.Threading;

public class Example18
{
    public static void Main18()
    {
        string[] values = { "able", "ångström", "apple", "Æble",
                          "Windows", "Visual Studio" };
        // Change thread to en-US.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        // Sort the array and copy it to a new array to preserve the order.
        Array.Sort(values);
        string[] enValues = (String[])values.Clone();

        // Change culture to Swedish (Sweden).
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("sv-SE");
        Array.Sort(values);
        string[] svValues = (String[])values.Clone();

        // Compare the sorted arrays.
        Console.WriteLine("{0,-8} {1,-15} {2,-15}\n", "Position", "en-US", "sv-SE");
        for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
            Console.WriteLine("{0,-8} {1,-15} {2,-15}", ctr, enValues[ctr], svValues[ctr]);
    }
}

// The example displays the following output:
//       Position en-US           sv-SE
//
//       0        able            able
//       1        Æble            Æble
//       2        ångström        apple
//       3        apple           Windows
//       4        Visual Studio   Visual Studio
//       5        Windows         ångström
Imports System.Globalization
Imports System.Threading

Module Example18
    Public Sub Main18()
        Dim values() As String = {"able", "ångström", "apple",
                                   "Æble", "Windows", "Visual Studio"}
        ' Change thread to en-US.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        ' Sort the array and copy it to a new array to preserve the order.
        Array.Sort(values)
        Dim enValues() As String = CType(values.Clone(), String())

        ' Change culture to Swedish (Sweden).
        Thread.CurrentThread.CurrentCulture = New CultureInfo("sv-SE")
        Array.Sort(values)
        Dim svValues() As String = CType(values.Clone(), String())

        ' Compare the sorted arrays.
        Console.WriteLine("{0,-8} {1,-15} {2,-15}", "Position", "en-US", "sv-SE")
        Console.WriteLine()
        For ctr As Integer = 0 To values.GetUpperBound(0)
            Console.WriteLine("{0,-8} {1,-15} {2,-15}", ctr, enValues(ctr), svValues(ctr))
        Next
    End Sub
End Module
' The example displays the following output:
'       Position en-US           sv-SE
'       
'       0        able            able
'       1        Æble            Æble
'       2        ångström        apple
'       3        apple           Windows
'       4        Visual Studio   Visual Studio
'       5        Windows         ångström

カルチャに依存した文字列比較は、各カルチャの CompareInfo プロパティによって返される CultureInfo.CompareInfo オブジェクトによって定義されます。 String.Compare メソッド オーバーロードを使用するカルチャに依存した文字列比較では、CompareInfo オブジェクトも使用します。

.NET は、文字列データのカルチャに依存した並べ替えを実行するためにテーブルを使用します。 並べ替えのウェイトと文字列の正規化に関するデータが入ったこれらのテーブルの内容は、特定のバージョンの .NET によって実装される Unicode 標準のバージョンによって決まります。 次の表は、特定のバージョンの .NET によって実装される Unicode のバージョンです。 サポートされている Unicode バージョンの一覧は、文字の比較と並べ替えに対してのみ適用されます。カテゴリ別での Unicode 文字の分類には適用されません。 詳細については、記事「String」 の「Strings and The Unicode Standard」 (文字列と Unicode 標準) セクションを参照してください。

.NET Framework のバージョン オペレーティング システム Unicode バージョン
.NET Framework 2.0 すべてのオペレーティング システム Unicode 4.1
.NET Framework 3.0 すべてのオペレーティング システム Unicode 4.1
.NET Framework 3.5 すべてのオペレーティング システム Unicode 4.1
.NET Framework 4 すべてのオペレーティング システム Unicode 5.0
.NET Framework 4.5 以降 Windows 7 Unicode 5.0
.NET Framework 4.5 以降 Windows 8 以降のオペレーティング システム Unicode 6.3.0
.NET Core および .NET 5+ 基になる OS でサポートされている Unicode 標準のバージョンによって異なります。

.NET Framework 4.5 以降と .NET Core および .NET 5+ のすべてのバージョンでは、文字列の比較と並べ替えはオペレーティング システムによって異なります。 Windows 7 で実行される NET Framework 4.5 以降は、Unicode 5.0 を実装する独自のテーブルからデータを取得します。 Windows 8 以降で実行される NET Framework 4.5 以降は、Unicode 6.3 を実装するオペレーティング システムのテーブルからデータを取得します。 .NET Core と .NET 5+ では、サポートされている Unicode のバージョンは基になるオペレーティング システムによって異なります。 カルチャに依存した並べ替えが実行されたデータをシリアル化する場合は、SortVersion クラスを使用して、.NET およびオペレーティング システムの並べ替え順序と一致するように、シリアル化されたデータをいつ並べ替える必要があるかを判断できます。 例については、SortVersion クラスに関するトピックを参照してください。

広範なカルチャ固有の並べ替えを文字列データに対して実行するアプリの場合、SortKey クラスを使用して文字列を比較できます。 並べ替えキーは、特定の文字列のアルファベット順、大文字と小文字の区別、発音の区別など、カルチャ固有の並べ替えウェイトを反映しています。 並べ替えキーを使用した比較はバイナリであるため、CompareInfo オブジェクトを暗黙的または明示的に使用する比較よりも高速です。 CompareInfo.GetSortKey メソッドに文字列を渡すことによって、特定の文字列のカルチャ固有の並べ替えキーを作成します。

次の例は、前の例と似ています。 ただし、暗黙的に Array.Sort(Array) メソッドを呼び出す CompareInfo.Compare メソッドを呼び出すのではなく、インスタンス化して、System.Collections.Generic.IComparer<T> メソッドに渡す、並べ替えキーを比較する Array.Sort<T>(T[], IComparer<T>) 実装を定義しています。

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;

public class SortKeyComparer : IComparer<String>
{
    public int Compare(string? str1, string? str2)
    {
        return (str1, str2) switch
        {
            (null, null) => 0,
            (null, _) => -1,
            (_, null) => 1,
            (var s1, var s2) => SortKey.Compare(
                CultureInfo.CurrentCulture.CompareInfo.GetSortKey(s1),
                CultureInfo.CurrentCulture.CompareInfo.GetSortKey(s1))
        };
    }
}

public class Example19
{
    public static void Main19()
    {
        string[] values = { "able", "ångström", "apple", "Æble",
                          "Windows", "Visual Studio" };
        SortKeyComparer comparer = new SortKeyComparer();

        // Change thread to en-US.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        // Sort the array and copy it to a new array to preserve the order.
        Array.Sort(values, comparer);
        string[] enValues = (String[])values.Clone();

        // Change culture to Swedish (Sweden).
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("sv-SE");
        Array.Sort(values, comparer);
        string[] svValues = (String[])values.Clone();

        // Compare the sorted arrays.
        Console.WriteLine("{0,-8} {1,-15} {2,-15}\n", "Position", "en-US", "sv-SE");
        for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
            Console.WriteLine("{0,-8} {1,-15} {2,-15}", ctr, enValues[ctr], svValues[ctr]);
    }
}

// The example displays the following output:
//       Position en-US           sv-SE
//
//       0        able            able
//       1        Æble            Æble
//       2        ångström        apple
//       3        apple           Windows
//       4        Visual Studio   Visual Studio
//       5        Windows         ångström
Imports System.Collections.Generic
Imports System.Globalization
Imports System.Threading

Public Class SortKeyComparer : Implements IComparer(Of String)
    Public Function Compare(str1 As String, str2 As String) As Integer _
           Implements IComparer(Of String).Compare
        Dim sk1, sk2 As SortKey
        sk1 = CultureInfo.CurrentCulture.CompareInfo.GetSortKey(str1)
        sk2 = CultureInfo.CurrentCulture.CompareInfo.GetSortKey(str2)
        Return SortKey.Compare(sk1, sk2)
    End Function
End Class

Module Example19
    Public Sub Main19()
        Dim values() As String = {"able", "ångström", "apple",
                                   "Æble", "Windows", "Visual Studio"}
        Dim comparer As New SortKeyComparer()

        ' Change thread to en-US.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        ' Sort the array and copy it to a new array to preserve the order.
        Array.Sort(values, comparer)
        Dim enValues() As String = CType(values.Clone(), String())

        ' Change culture to Swedish (Sweden).
        Thread.CurrentThread.CurrentCulture = New CultureInfo("sv-SE")
        Array.Sort(values, comparer)
        Dim svValues() As String = CType(values.Clone(), String())

        ' Compare the sorted arrays.
        Console.WriteLine("{0,-8} {1,-15} {2,-15}", "Position", "en-US", "sv-SE")
        Console.WriteLine()
        For ctr As Integer = 0 To values.GetUpperBound(0)
            Console.WriteLine("{0,-8} {1,-15} {2,-15}", ctr, enValues(ctr), svValues(ctr))
        Next
    End Sub
End Module
' The example displays the following output:
'       Position en-US           sv-SE
'       
'       0        able            able
'       1        Æble            Æble
'       2        ångström        apple
'       3        apple           Windows
'       4        Visual Studio   Visual Studio
'       5        Windows         ångström

文字列の連結を回避する

可能な限り、文字列を実行時に連結して使う方法は避けてください。 連結される文字列は、他のローカライズ言語には当てはまらない、アプリの元の言語の語順に依存することが多く、ローカライズ作業が困難になります。

日付と時刻を処理する

日付と時刻の値を処理する方法は、その値をユーザー インターフェイスに表示するのか、保持するのかによって異なります。 このセクションでは、両方の使用を検討します。 また、日付と時刻を操作するときにタイム ゾーンの相違と算術演算を処理する方法についても説明します。

日付と時刻を表示する

通常、日付と時刻をユーザー インターフェイスに表示するときには、CultureInfo.CurrentCulture プロパティと、DateTimeFormatInfo プロパティによって返される CultureInfo.CurrentCulture.DateTimeFormat オブジェクトで定義されているユーザーのカルチャの書式指定規則を使用する必要があります。 次のメソッドのいずれかを使用して日付の書式を設定すると、現在のカルチャの書式指定規則が自動的に使用されます。

次の例では、2012 年 10 月 11 日の日の出と日没のデータを 2 回表示します。 最初に、現在のカルチャをクロアチア語 (クロアチア) に設定し、次に英語 (英国) に設定します。 どちらの場合も、日付と時刻はそのカルチャに適した書式で表示されます。

using System;
using System.Globalization;
using System.Threading;

public class Example3
{
    static DateTime[] dates = { new DateTime(2012, 10, 11, 7, 06, 0),
                        new DateTime(2012, 10, 11, 18, 19, 0) };

    public static void Main3()
    {
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("hr-HR");
        ShowDayInfo();
        Console.WriteLine();
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
        ShowDayInfo();
    }

    private static void ShowDayInfo()
    {
        Console.WriteLine("Date: {0:D}", dates[0]);
        Console.WriteLine("   Sunrise: {0:T}", dates[0]);
        Console.WriteLine("   Sunset:  {0:T}", dates[1]);
    }
}

// The example displays the following output:
//       Date: 11. listopada 2012.
//          Sunrise: 7:06:00
//          Sunset:  18:19:00
//
//       Date: 11 October 2012
//          Sunrise: 07:06:00
//          Sunset:  18:19:00
Imports System.Globalization
Imports System.Threading

Module Example3
    Dim dates() As Date = {New Date(2012, 10, 11, 7, 6, 0),
                            New Date(2012, 10, 11, 18, 19, 0)}

    Public Sub Main3()
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("hr-HR")
        ShowDayInfo()
        Console.WriteLine()
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
        ShowDayInfo()
    End Sub

    Private Sub ShowDayInfo()
        Console.WriteLine("Date: {0:D}", dates(0))
        Console.WriteLine("   Sunrise: {0:T}", dates(0))
        Console.WriteLine("   Sunset:  {0:T}", dates(1))
    End Sub
End Module
' The example displays the following output:
'       Date: 11. listopada 2012.
'          Sunrise: 7:06:00
'          Sunset:  18:19:00
'       
'       Date: 11 October 2012
'          Sunrise: 07:06:00
'          Sunset:  18:19:00

日付と時刻を保持する

日付と時刻のデータはカルチャによって異なる可能性がある書式で保持しないでください。 これは、データの破損または実行時例外が発生する一般的なプログラミング エラーです。 次の例では、英語 (米国) カルチャの書式指定規則を使用して文字列として 2 つの日付 (2013 年 1 月 9 日と 2013 年 8 月 18 日) をシリアル化します。 データが英語 (米国) カルチャの規則を使用して取得および解析されると、正常に復元されます。 ただし、英語 (イギリス) カルチャの規則を使用して取得および解析されると、最初の日付は 9 月 1 日と間違って解釈され、グレゴリオ暦には 18 番目の月がないため 2 番目の日付は解析されません。

using System;
using System.IO;
using System.Globalization;
using System.Threading;

public class Example4
{
    public static void Main4()
    {
        // Persist two dates as strings.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        DateTime[] dates = { new DateTime(2013, 1, 9),
                           new DateTime(2013, 8, 18) };
        StreamWriter sw = new StreamWriter("dateData.dat");
        sw.Write("{0:d}|{1:d}", dates[0], dates[1]);
        sw.Close();

        // Read the persisted data.
        StreamReader sr = new StreamReader("dateData.dat");
        string dateData = sr.ReadToEnd();
        sr.Close();
        string[] dateStrings = dateData.Split('|');

        // Restore and display the data using the conventions of the en-US culture.
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName);
        foreach (var dateStr in dateStrings)
        {
            DateTime restoredDate;
            if (DateTime.TryParse(dateStr, out restoredDate))
                Console.WriteLine("The date is {0:D}", restoredDate);
            else
                Console.WriteLine("ERROR: Unable to parse {0}", dateStr);
        }
        Console.WriteLine();

        // Restore and display the data using the conventions of the en-GB culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName);
        foreach (var dateStr in dateStrings)
        {
            DateTime restoredDate;
            if (DateTime.TryParse(dateStr, out restoredDate))
                Console.WriteLine("The date is {0:D}", restoredDate);
            else
                Console.WriteLine("ERROR: Unable to parse {0}", dateStr);
        }
    }
}

// The example displays the following output:
//       Current Culture: English (United States)
//       The date is Wednesday, January 09, 2013
//       The date is Sunday, August 18, 2013
//
//       Current Culture: English (United Kingdom)
//       The date is 01 September 2013
//       ERROR: Unable to parse 8/18/2013
Imports System.Globalization
Imports System.IO
Imports System.Threading

Module Example4
    Public Sub Main4()
        ' Persist two dates as strings.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        Dim dates() As DateTime = {New DateTime(2013, 1, 9),
                                    New DateTime(2013, 8, 18)}
        Dim sw As New StreamWriter("dateData.dat")
        sw.Write("{0:d}|{1:d}", dates(0), dates(1))
        sw.Close()

        ' Read the persisted data.
        Dim sr As New StreamReader("dateData.dat")
        Dim dateData As String = sr.ReadToEnd()
        sr.Close()
        Dim dateStrings() As String = dateData.Split("|"c)

        ' Restore and display the data using the conventions of the en-US culture.
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        For Each dateStr In dateStrings
            Dim restoredDate As Date
            If Date.TryParse(dateStr, restoredDate) Then
                Console.WriteLine("The date is {0:D}", restoredDate)
            Else
                Console.WriteLine("ERROR: Unable to parse {0}", dateStr)
            End If
        Next
        Console.WriteLine()

        ' Restore and display the data using the conventions of the en-GB culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        For Each dateStr In dateStrings
            Dim restoredDate As Date
            If Date.TryParse(dateStr, restoredDate) Then
                Console.WriteLine("The date is {0:D}", restoredDate)
            Else
                Console.WriteLine("ERROR: Unable to parse {0}", dateStr)
            End If
        Next
    End Sub
End Module
' The example displays the following output:
'       Current Culture: English (United States)
'       The date is Wednesday, January 09, 2013
'       The date is Sunday, August 18, 2013
'       
'       Current Culture: English (United Kingdom)
'       The date is 01 September 2013
'       ERROR: Unable to parse 8/18/2013

この問題は、次の 3 つのうちいずれかの方法で回避できます。

  • 文字列ではなくバイナリ形式で日付と時刻をシリアル化します。
  • ユーザーのカルチャに関係なく同じであるカスタム書式指定文字列を使用して日付と時刻の文字列表現を保存および解析します。
  • インバリアント カルチャの書式指定規則を使用して文字列を保存します。

次の例では、最後のアプローチを示します。 この例では、静的 CultureInfo.InvariantCulture プロパティによって返されるインバリアント カルチャの書式指定規則を使用します。

using System;
using System.IO;
using System.Globalization;
using System.Threading;

public class Example5
{
    public static void Main5()
    {
        // Persist two dates as strings.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        DateTime[] dates = { new DateTime(2013, 1, 9),
                           new DateTime(2013, 8, 18) };
        StreamWriter sw = new StreamWriter("dateData.dat");
        sw.Write(String.Format(CultureInfo.InvariantCulture,
                               "{0:d}|{1:d}", dates[0], dates[1]));
        sw.Close();

        // Read the persisted data.
        StreamReader sr = new StreamReader("dateData.dat");
        string dateData = sr.ReadToEnd();
        sr.Close();
        string[] dateStrings = dateData.Split('|');

        // Restore and display the data using the conventions of the en-US culture.
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName);
        foreach (var dateStr in dateStrings)
        {
            DateTime restoredDate;
            if (DateTime.TryParse(dateStr, CultureInfo.InvariantCulture,
                                  DateTimeStyles.None, out restoredDate))
                Console.WriteLine("The date is {0:D}", restoredDate);
            else
                Console.WriteLine("ERROR: Unable to parse {0}", dateStr);
        }
        Console.WriteLine();

        // Restore and display the data using the conventions of the en-GB culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName);
        foreach (var dateStr in dateStrings)
        {
            DateTime restoredDate;
            if (DateTime.TryParse(dateStr, CultureInfo.InvariantCulture,
                                  DateTimeStyles.None, out restoredDate))
                Console.WriteLine("The date is {0:D}", restoredDate);
            else
                Console.WriteLine("ERROR: Unable to parse {0}", dateStr);
        }
    }
}

// The example displays the following output:
//       Current Culture: English (United States)
//       The date is Wednesday, January 09, 2013
//       The date is Sunday, August 18, 2013
//
//       Current Culture: English (United Kingdom)
//       The date is 09 January 2013
//       The date is 18 August 2013
Imports System.Globalization
Imports System.IO
Imports System.Threading

Module Example5
    Public Sub Main5()
        ' Persist two dates as strings.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        Dim dates() As DateTime = {New DateTime(2013, 1, 9),
                                    New DateTime(2013, 8, 18)}
        Dim sw As New StreamWriter("dateData.dat")
        sw.Write(String.Format(CultureInfo.InvariantCulture,
                               "{0:d}|{1:d}", dates(0), dates(1)))
        sw.Close()

        ' Read the persisted data.
        Dim sr As New StreamReader("dateData.dat")
        Dim dateData As String = sr.ReadToEnd()
        sr.Close()
        Dim dateStrings() As String = dateData.Split("|"c)

        ' Restore and display the data using the conventions of the en-US culture.
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        For Each dateStr In dateStrings
            Dim restoredDate As Date
            If Date.TryParse(dateStr, CultureInfo.InvariantCulture,
                             DateTimeStyles.None, restoredDate) Then
                Console.WriteLine("The date is {0:D}", restoredDate)
            Else
                Console.WriteLine("ERROR: Unable to parse {0}", dateStr)
            End If
        Next
        Console.WriteLine()

        ' Restore and display the data using the conventions of the en-GB culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        For Each dateStr In dateStrings
            Dim restoredDate As Date
            If Date.TryParse(dateStr, CultureInfo.InvariantCulture,
                             DateTimeStyles.None, restoredDate) Then
                Console.WriteLine("The date is {0:D}", restoredDate)
            Else
                Console.WriteLine("ERROR: Unable to parse {0}", dateStr)
            End If
        Next
    End Sub
End Module
' The example displays the following output:
'       Current Culture: English (United States)
'       The date is Wednesday, January 09, 2013
'       The date is Sunday, August 18, 2013
'       
'       Current Culture: English (United Kingdom)
'       The date is 09 January 2013
'       The date is 18 August 2013

シリアル化とタイム ゾーンへの対応

日付と時刻の値は、一般的な時刻 ("店舗は 2013 年 1 月 2 日の午前 9 時に開店します") から特定の時点 ("生年月日:2013 年 1 月 2 日午前 6 時 32 分 00 秒") まで、多様に解釈できます。 時刻の値が特定の時点を表し、それをシリアル化された値から復元する場合、ユーザーの地理的場所またはタイム ゾーンに関係なく同じ特定の時点を表すことを確認する必要があります。

この問題を説明する例を次に示します。 1 つのローカル日付と時刻の値を、3 つの標準形式の文字列として保存します。

  • 一般の日付と長い形式の時刻の場合は "G"。
  • 並べ替え可能な日付/時刻の場合は "s"。
  • ラウンドトリップする日付/時刻の場合は "o"。
using System;
using System.IO;

public class Example6
{
    public static void Main6()
    {
        DateTime dateOriginal = new DateTime(2023, 3, 30, 18, 0, 0);
        dateOriginal = DateTime.SpecifyKind(dateOriginal, DateTimeKind.Local);

        // Serialize a date.
        if (!File.Exists("DateInfo.dat"))
        {
            StreamWriter sw = new StreamWriter("DateInfo.dat");
            sw.Write("{0:G}|{0:s}|{0:o}", dateOriginal);
            sw.Close();
            Console.WriteLine("Serialized dates to DateInfo.dat");
        }
        Console.WriteLine();

        // Restore the date from string values.
        StreamReader sr = new StreamReader("DateInfo.dat");
        string datesToSplit = sr.ReadToEnd();
        string[] dateStrings = datesToSplit.Split('|');
        foreach (var dateStr in dateStrings)
        {
            DateTime newDate = DateTime.Parse(dateStr);
            Console.WriteLine("'{0}' --> {1} {2}",
                              dateStr, newDate, newDate.Kind);
        }
    }
}
Imports System.IO

Module Example6
    Public Sub Main6()
        ' Serialize a date.
        Dim dateOriginal As Date = #03/30/2023 6:00PM#
        dateOriginal = DateTime.SpecifyKind(dateOriginal, DateTimeKind.Local)
        ' Serialize the date in string form.
        If Not File.Exists("DateInfo.dat") Then
            Dim sw As New StreamWriter("DateInfo.dat")
            sw.Write("{0:G}|{0:s}|{0:o}", dateOriginal)
            sw.Close()
        End If

        ' Restore the date from string values.
        Dim sr As New StreamReader("DateInfo.dat")
        Dim datesToSplit As String = sr.ReadToEnd()
        Dim dateStrings() As String = datesToSplit.Split("|"c)
        For Each dateStr In dateStrings
            Dim newDate As DateTime = DateTime.Parse(dateStr)
            Console.WriteLine("'{0}' --> {1} {2}",
                              dateStr, newDate, newDate.Kind)
        Next
    End Sub
End Module

シリアル化されたシステムと同じタイム ゾーンのシステムでデータを復元すると、出力に示すように、逆シリアル化された日付と時刻の値は正確に元の値を反映しています。

'3/30/2013 6:00:00 PM' --> 3/30/2013 6:00:00 PM Unspecified
'2013-03-30T18:00:00' --> 3/30/2013 6:00:00 PM Unspecified
'2013-03-30T18:00:00.0000000-07:00' --> 3/30/2013 6:00:00 PM Local

ただし、別のタイム ゾーンのシステムでデータを復元する場合は、"o" (ラウンド トリップ) 標準書式指定文字列を使用して書式設定された日付と時刻の値だけがタイム ゾーン情報を維持して、同じ時点を表します。 日付と時刻のデータがロマンス標準時ゾーンのシステムで復元されたときの出力を次に示します。

'3/30/2023 6:00:00 PM' --> 3/30/2023 6:00:00 PM Unspecified
'2023-03-30T18:00:00' --> 3/30/2023 6:00:00 PM Unspecified
'2023-03-30T18:00:00.0000000-07:00' --> 3/31/2023 3:00:00 AM Local

データが逆シリアル化されるシステムのタイム ゾーンに関係なく 1 つの時点を表す日付と時刻の値を正確に反映するには、次のいずれかの方法を使用できます。

  • "o" (ラウンド トリップ) 標準書式指定文字列を使用して値を文字列として保存します。 その後、ターゲット システムで逆シリアル化します。
  • 値を UTC に変換し、"r" (RFC1123) 標準書式指定文字列を使用して文字列として保存します。 その後、ターゲット システムで逆シリアル化し、現地時刻に変換します。
  • 値を UTC に変換し、"u" (世界共通の並べ替え可能な日付と時刻) 標準書式指定文字列を使用して文字列として保存します。 その後、ターゲット システムで逆シリアル化し、現地時刻に変換します。

各方法の例を次に示します。

using System;
using System.IO;

public class Example9
{
    public static void Main9()
    {
        // Serialize a date.
        DateTime dateOriginal = new DateTime(2023, 3, 30, 18, 0, 0);
        dateOriginal = DateTime.SpecifyKind(dateOriginal, DateTimeKind.Local);

        // Serialize the date in string form.
        if (!File.Exists("DateInfo2.dat"))
        {
            StreamWriter sw = new StreamWriter("DateInfo2.dat");
            sw.Write("{0:o}|{1:r}|{1:u}", dateOriginal,
                                          dateOriginal.ToUniversalTime());
            sw.Close();
        }

        // Restore the date from string values.
        StreamReader sr = new StreamReader("DateInfo2.dat");
        string datesToSplit = sr.ReadToEnd();
        string[] dateStrings = datesToSplit.Split('|');
        for (int ctr = 0; ctr < dateStrings.Length; ctr++)
        {
            DateTime newDate = DateTime.Parse(dateStrings[ctr]);
            if (ctr == 1)
            {
                Console.WriteLine($"'{dateStrings[ctr]}' --> {newDate} {newDate.Kind}");
            }
            else
            {
                DateTime newLocalDate = newDate.ToLocalTime();
                Console.WriteLine($"'{dateStrings[ctr]}' --> {newLocalDate} {newLocalDate.Kind}");
            }
        }
    }
}
Imports System.IO

Module Example9
    Public Sub Main9()
        ' Serialize a date.
        Dim dateOriginal As Date = #03/30/2023 6:00PM#
        dateOriginal = DateTime.SpecifyKind(dateOriginal, DateTimeKind.Local)

        ' Serialize the date in string form.
        If Not File.Exists("DateInfo2.dat") Then
            Dim sw As New StreamWriter("DateInfo2.dat")
            sw.Write("{0:o}|{1:r}|{1:u}", dateOriginal,
                                          dateOriginal.ToUniversalTime())
            sw.Close()
        End If

        ' Restore the date from string values.
        Dim sr As New StreamReader("DateInfo2.dat")
        Dim datesToSplit As String = sr.ReadToEnd()
        Dim dateStrings() As String = datesToSplit.Split("|"c)
        For ctr As Integer = 0 To dateStrings.Length - 1
            Dim newDate As DateTime = DateTime.Parse(dateStrings(ctr))
            If ctr = 1 Then
                Console.WriteLine("'{0}' --> {1} {2}",
                                  dateStrings(ctr), newDate, newDate.Kind)
            Else
                Dim newLocalDate As DateTime = newDate.ToLocalTime()
                Console.WriteLine("'{0}' --> {1} {2}",
                                  dateStrings(ctr), newLocalDate, newLocalDate.Kind)
            End If
        Next
    End Sub
End Module

データを太平洋標準時ゾーンのシステムでシリアル化し、ロマンス標準時ゾーンのシステムで逆シリアル化すると、次の出力が表示されます。

'2023-03-30T18:00:00.0000000-07:00' --> 3/31/2023 3:00:00 AM Local
'Sun, 31 Mar 2023 01:00:00 GMT' --> 3/31/2023 3:00:00 AM Local
'2023-03-31 01:00:00Z' --> 3/31/2023 3:00:00 AM Local

詳細については、「タイム ゾーン間での時刻の変換」を参照してください。

日付と時刻の演算を実行する

DateTime 型と DateTimeOffset 型は、算術演算をサポートします。 2 つの日付の値の差を計算したり、日付の値に特定の時間間隔を加算または減算したりできます。 ただし、日付と時刻の値に対する算術演算では、タイム ゾーンとタイム ゾーン調整規則が考慮されません。 このため、時点を表す値に対して日付と時刻の算術を実行すると、不正確な結果が返されることがあります。

たとえば、太平洋標準時から太平洋夏時間への切り替えは、3 月の第 2 日曜日、2013 年の場合は 3 月 10 日に行われます。 次の例に示すように、太平洋標準時ゾーンのシステムで、2013 年 3 月 9 日午前 10 時 30 分の 48 時間後に当たる日付と時刻を計算した場合、結果は 2013 年 3 月 11 日午前 10 時 30 分となり、その間の時間調整は考慮されません。

using System;

public class Example7
{
    public static void Main7()
    {
        DateTime date1 = DateTime.SpecifyKind(new DateTime(2013, 3, 9, 10, 30, 0),
                                              DateTimeKind.Local);
        TimeSpan interval = new TimeSpan(48, 0, 0);
        DateTime date2 = date1 + interval;
        Console.WriteLine("{0:g} + {1:N1} hours = {2:g}",
                          date1, interval.TotalHours, date2);
    }
}

// The example displays the following output:
//        3/9/2013 10:30 AM + 48.0 hours = 3/11/2013 10:30 AM
Module Example7
    Public Sub Main7()
        Dim date1 As Date = DateTime.SpecifyKind(#3/9/2013 10:30AM#,
                                                 DateTimeKind.Local)
        Dim interval As New TimeSpan(48, 0, 0)
        Dim date2 As Date = date1 + interval
        Console.WriteLine("{0:g} + {1:N1} hours = {2:g}",
                          date1, interval.TotalHours, date2)
    End Sub
End Module
' The example displays the following output:
'       3/9/2013 10:30 AM + 48.0 hours = 3/11/2013 10:30 AM

日付と時刻の値に対する算術演算によって正確な結果を生成するには、次の手順を実行します。

  1. ソース タイム ゾーンの時刻を UTC に変換します。
  2. 算術演算を実行します。
  3. 結果が日付と時刻の値である場合、UTC からソース タイム ゾーンの時刻に変換します。

次の例は前の例と似ていますが、この 3 つの手順を実行することで、2013 年 3 月 9 日午前 10 時 30 分に 48 時間が正しく追加されています。

using System;

public class Example8
{
    public static void Main8()
    {
        TimeZoneInfo pst = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
        DateTime date1 = DateTime.SpecifyKind(new DateTime(2013, 3, 9, 10, 30, 0),
                                              DateTimeKind.Local);
        DateTime utc1 = date1.ToUniversalTime();
        TimeSpan interval = new TimeSpan(48, 0, 0);
        DateTime utc2 = utc1 + interval;
        DateTime date2 = TimeZoneInfo.ConvertTimeFromUtc(utc2, pst);
        Console.WriteLine("{0:g} + {1:N1} hours = {2:g}",
                          date1, interval.TotalHours, date2);
    }
}

// The example displays the following output:
//        3/9/2013 10:30 AM + 48.0 hours = 3/11/2013 11:30 AM
Module Example8
    Public Sub Main8()
        Dim pst As TimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time")
        Dim date1 As Date = DateTime.SpecifyKind(#3/9/2013 10:30AM#,
                                                 DateTimeKind.Local)
        Dim utc1 As Date = date1.ToUniversalTime()
        Dim interval As New TimeSpan(48, 0, 0)
        Dim utc2 As Date = utc1 + interval
        Dim date2 As Date = TimeZoneInfo.ConvertTimeFromUtc(utc2, pst)
        Console.WriteLine("{0:g} + {1:N1} hours = {2:g}",
                          date1, interval.TotalHours, date2)
    End Sub
End Module
' The example displays the following output:
'       3/9/2013 10:30 AM + 48.0 hours = 3/11/2013 11:30 AM

詳細については、「日付と時刻を使用した算術演算の実行」を参照してください。

日付要素にカルチャに依存した名前を使用する

アプリによっては、月または曜日の名前を表示することが必要になる場合があります。 これを実行するために、次のようなコードが一般に使用されます。

using System;

public class Example12
{
   public static void Main12()
   {
      DateTime midYear = new DateTime(2013, 7, 1);
      Console.WriteLine("{0:d} is a {1}.", midYear, GetDayName(midYear));
   }

   private static string GetDayName(DateTime date)
   {
      return date.DayOfWeek.ToString("G");
   }
}

// The example displays the following output:
//        7/1/2013 is a Monday.
Module Example12
    Public Sub Main12()
        Dim midYear As Date = #07/01/2013#
        Console.WriteLine("{0:d} is a {1}.", midYear, GetDayName(midYear))
    End Sub

    Private Function GetDayName(dat As Date) As String
        Return dat.DayOfWeek.ToString("G")
    End Function
End Module
' The example displays the following output:
'       7/1/2013 is a Monday.

ただし、このコードは曜日の名前を必ず英語で返します。 多くの場合、月の名前を抽出するコードには、さらに柔軟性がありません。 一般に、このようなコードでは特定の言語の月の名前を使用した 12 か月の暦を前提としています。

次の例に示すように、カスタム日時書式指定文字列または DateTimeFormatInfo オブジェクトのプロパティを使用すると、ユーザーのカルチャの曜日または月の名前を反映する文字列を簡単に抽出できます。 この例では、現在のカルチャをフランス語 (フランス) に変更し、2013 年 7 月 1 日の曜日の名前と月の名前を表示します。

using System;
using System.Globalization;

public class Example13
{
    public static void Main13()
    {
        // Set the current culture to French (France).
        CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR");

        DateTime midYear = new DateTime(2013, 7, 1);
        Console.WriteLine("{0:d} is a {1}.", midYear, DateUtilities.GetDayName(midYear));
        Console.WriteLine("{0:d} is a {1}.", midYear, DateUtilities.GetDayName((int)midYear.DayOfWeek));
        Console.WriteLine("{0:d} is in {1}.", midYear, DateUtilities.GetMonthName(midYear));
        Console.WriteLine("{0:d} is in {1}.", midYear, DateUtilities.GetMonthName(midYear.Month));
    }
}

public static class DateUtilities
{
    public static string GetDayName(int dayOfWeek)
    {
        if (dayOfWeek < 0 | dayOfWeek > DateTimeFormatInfo.CurrentInfo.DayNames.Length)
            return String.Empty;
        else
            return DateTimeFormatInfo.CurrentInfo.DayNames[dayOfWeek];
    }

    public static string GetDayName(DateTime date)
    {
        return date.ToString("dddd");
    }

    public static string GetMonthName(int month)
    {
        if (month < 1 | month > DateTimeFormatInfo.CurrentInfo.MonthNames.Length - 1)
            return String.Empty;
        else
            return DateTimeFormatInfo.CurrentInfo.MonthNames[month - 1];
    }

    public static string GetMonthName(DateTime date)
    {
        return date.ToString("MMMM");
    }
}

// The example displays the following output:
//       01/07/2013 is a lundi.
//       01/07/2013 is a lundi.
//       01/07/2013 is in juillet.
//       01/07/2013 is in juillet.
Imports System.Globalization
Imports System.Threading

Module Example13
    Public Sub Main13()
        ' Set the current culture to French (France).
        CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR")

        Dim midYear As Date = #07/01/2013#
        Console.WriteLine("{0:d} is a {1}.", midYear, DateUtilities.GetDayName(midYear))
        Console.WriteLine("{0:d} is a {1}.", midYear, DateUtilities.GetDayName(midYear.DayOfWeek))
        Console.WriteLine("{0:d} is in {1}.", midYear, DateUtilities.GetMonthName(midYear))
        Console.WriteLine("{0:d} is in {1}.", midYear, DateUtilities.GetMonthName(midYear.Month))
    End Sub
End Module

Public Class DateUtilities
    Public Shared Function GetDayName(dayOfWeek As Integer) As String
        If dayOfWeek < 0 Or dayOfWeek > DateTimeFormatInfo.CurrentInfo.DayNames.Length Then
            Return String.Empty
        Else
            Return DateTimeFormatInfo.CurrentInfo.DayNames(dayOfWeek)
        End If
    End Function

    Public Shared Function GetDayName(dat As Date) As String
        Return dat.ToString("dddd")
    End Function

    Public Shared Function GetMonthName(month As Integer) As String
        If month < 1 Or month > DateTimeFormatInfo.CurrentInfo.MonthNames.Length - 1 Then
            Return String.Empty
        Else
            Return DateTimeFormatInfo.CurrentInfo.MonthNames(month - 1)
        End If
    End Function

    Public Shared Function GetMonthName(dat As Date) As String
        Return dat.ToString("MMMM")
    End Function
End Class
' The example displays the following output:
'       01/07/2013 is a lundi.
'       01/07/2013 is a lundi.
'       01/07/2013 is in juillet.
'       01/07/2013 is in juillet.

数値

数値の処理は、数値をユーザー インターフェイスに表示するのか、保持するのかによって異なります。 このセクションでは、両方の使用を検討します。

注意

解析操作と書式設定操作では、.NET は基本ラテン文字 0 から 9 (U+0030 から U+0039) だけを数字として認識します。

数値を表示する

通常、数値をユーザー インターフェイスに表示するときには、CultureInfo.CurrentCulture プロパティと、NumberFormatInfo プロパティによって返される CultureInfo.CurrentCulture.NumberFormat オブジェクトで定義されているユーザーのカルチャの書式指定規則を使用する必要があります。 次のいずれかの方法で日付の書式を設定すると、現在のカルチャの書式指定規則が自動的に使用されます。

  • 任意の数値型のパラメーターなしの ToString メソッドの使用。
  • 書式指定文字列を引数として含む任意の数値型の ToString(String) メソッドの使用。
  • 数値を使用した複合書式の使用。

次の例では、パリ (フランス) の毎月の平均気温を表示します。 この例では、最初にデータを表示する前に現在のカルチャをフランス語 (フランス) に設定し、次に英語 (米国) に設定します。 どちらの場合も、月の名前と気温はそのカルチャに適した形式で表示されます。 この 2 つのカルチャでは、気温の値に使用する小数点記号が異なります。 また、この例では、"MMMM" カスタム日時書式指定文字列を使用して月の正式名を表示し、DateTimeFormatInfo.MonthNames 配列で最も長い月の名前の長さを確認することで、結果の文字列の月の名前に適切な領域を割り当てます。

using System;
using System.Globalization;
using System.Threading;

public class Example14
{
    public static void Main14()
    {
        DateTime dateForMonth = new DateTime(2013, 1, 1);
        double[] temperatures = {  3.4, 3.5, 7.6, 10.4, 14.5, 17.2,
                                19.9, 18.2, 15.9, 11.3, 6.9, 5.3 };

        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR");
        Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName);
        // Build the format string dynamically so we allocate enough space for the month name.
        string fmtString = "{0,-" + GetLongestMonthNameLength().ToString() + ":MMMM}     {1,4}";
        for (int ctr = 0; ctr < temperatures.Length; ctr++)
            Console.WriteLine(fmtString,
                              dateForMonth.AddMonths(ctr),
                              temperatures[ctr]);

        Console.WriteLine();

        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName);
        fmtString = "{0,-" + GetLongestMonthNameLength().ToString() + ":MMMM}     {1,4}";
        for (int ctr = 0; ctr < temperatures.Length; ctr++)
            Console.WriteLine(fmtString,
                              dateForMonth.AddMonths(ctr),
                              temperatures[ctr]);
    }

    private static int GetLongestMonthNameLength()
    {
        int length = 0;
        foreach (var nameOfMonth in DateTimeFormatInfo.CurrentInfo.MonthNames)
            if (nameOfMonth.Length > length) length = nameOfMonth.Length;

        return length;
    }
}

// The example displays the following output:
//    Current Culture: French (France)
//       janvier        3,4
//       février        3,5
//       mars           7,6
//       avril         10,4
//       mai           14,5
//       juin          17,2
//       juillet       19,9
//       août          18,2
//       septembre     15,9
//       octobre       11,3
//       novembre       6,9
//       décembre       5,3
//
//       Current Culture: English (United States)
//       January        3.4
//       February       3.5
//       March          7.6
//       April         10.4
//       May           14.5
//       June          17.2
//       July          19.9
//       August        18.2
//       September     15.9
//       October       11.3
//       November       6.9
//       December       5.3
Imports System.Globalization
Imports System.Threading

Module Example14
    Public Sub Main14()
        Dim dateForMonth As Date = #1/1/2013#
        Dim temperatures() As Double = {3.4, 3.5, 7.6, 10.4, 14.5, 17.2,
                                         19.9, 18.2, 15.9, 11.3, 6.9, 5.3}

        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR")
        Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
        Dim fmtString As String = "{0,-" + GetLongestMonthNameLength().ToString() + ":MMMM}     {1,4}"
        For ctr = 0 To temperatures.Length - 1
            Console.WriteLine(fmtString,
                              dateForMonth.AddMonths(ctr),
                              temperatures(ctr))
        Next
        Console.WriteLine()

        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
        ' Build the format string dynamically so we allocate enough space for the month name.
        fmtString = "{0,-" + GetLongestMonthNameLength().ToString() + ":MMMM}     {1,4}"
        For ctr = 0 To temperatures.Length - 1
            Console.WriteLine(fmtString,
                              dateForMonth.AddMonths(ctr),
                              temperatures(ctr))
        Next
    End Sub

    Private Function GetLongestMonthNameLength() As Integer
        Dim length As Integer
        For Each nameOfMonth In DateTimeFormatInfo.CurrentInfo.MonthNames
            If nameOfMonth.Length > length Then length = nameOfMonth.Length
        Next
        Return length
    End Function
End Module
' The example displays the following output:
'       Current Culture: French (France)
'       janvier        3,4
'       février        3,5
'       mars           7,6
'       avril         10,4
'       mai           14,5
'       juin          17,2
'       juillet       19,9
'       août          18,2
'       septembre     15,9
'       octobre       11,3
'       novembre       6,9
'       décembre       5,3
'       
'       Current Culture: English (United States)
'       January        3.4
'       February       3.5
'       March          7.6
'       April         10.4
'       May           14.5
'       June          17.2
'       July          19.9
'       August        18.2
'       September     15.9
'       October       11.3
'       November       6.9
'       December       5.3

数値を保持する

数値データはカルチャ固有の書式で保持しないでください。 これは、データの破損または実行時例外が発生する一般的なプログラミング エラーです。 次の例では、10 個のランダムな浮動小数点数を生成し、英語 (米国) カルチャの書式指定規則を使用して文字列としてシリアル化します。 データが英語 (米国) カルチャの規則を使用して取得および解析されると、正常に復元されます。 ただし、この数値をフランス語 (フランス) カルチャの規則を使用して取得および解析すると、使用する小数点記号が異なるため、数値はいずれも解析できません。

using System;
using System.Globalization;
using System.IO;
using System.Threading;

public class Example15
{
    public static void Main15()
    {
        // Create ten random doubles.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        double[] numbers = GetRandomNumbers(10);
        DisplayRandomNumbers(numbers);

        // Persist the numbers as strings.
        StreamWriter sw = new StreamWriter("randoms.dat");
        for (int ctr = 0; ctr < numbers.Length; ctr++)
            sw.Write("{0:R}{1}", numbers[ctr], ctr < numbers.Length - 1 ? "|" : "");

        sw.Close();

        // Read the persisted data.
        StreamReader sr = new StreamReader("randoms.dat");
        string numericData = sr.ReadToEnd();
        sr.Close();
        string[] numberStrings = numericData.Split('|');

        // Restore and display the data using the conventions of the en-US culture.
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName);
        foreach (var numberStr in numberStrings)
        {
            double restoredNumber;
            if (Double.TryParse(numberStr, out restoredNumber))
                Console.WriteLine(restoredNumber.ToString("R"));
            else
                Console.WriteLine("ERROR: Unable to parse '{0}'", numberStr);
        }
        Console.WriteLine();

        // Restore and display the data using the conventions of the fr-FR culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR");
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName);
        foreach (var numberStr in numberStrings)
        {
            double restoredNumber;
            if (Double.TryParse(numberStr, out restoredNumber))
                Console.WriteLine(restoredNumber.ToString("R"));
            else
                Console.WriteLine("ERROR: Unable to parse '{0}'", numberStr);
        }
    }

    private static double[] GetRandomNumbers(int n)
    {
        Random rnd = new Random();
        double[] numbers = new double[n];
        for (int ctr = 0; ctr < n; ctr++)
            numbers[ctr] = rnd.NextDouble() * 1000;
        return numbers;
    }

    private static void DisplayRandomNumbers(double[] numbers)
    {
        for (int ctr = 0; ctr < numbers.Length; ctr++)
            Console.WriteLine(numbers[ctr].ToString("R"));
        Console.WriteLine();
    }
}

// The example displays output like the following:
//       487.0313743534644
//       674.12000879371533
//       498.72077885024288
//       42.3034229512808
//       970.57311049223563
//       531.33717716268131
//       587.82905693530529
//       562.25210175023039
//       600.7711019370571
//       299.46113717717174
//
//       Current Culture: English (United States)
//       487.0313743534644
//       674.12000879371533
//       498.72077885024288
//       42.3034229512808
//       970.57311049223563
//       531.33717716268131
//       587.82905693530529
//       562.25210175023039
//       600.7711019370571
//       299.46113717717174
//
//       Current Culture: French (France)
//       ERROR: Unable to parse '487.0313743534644'
//       ERROR: Unable to parse '674.12000879371533'
//       ERROR: Unable to parse '498.72077885024288'
//       ERROR: Unable to parse '42.3034229512808'
//       ERROR: Unable to parse '970.57311049223563'
//       ERROR: Unable to parse '531.33717716268131'
//       ERROR: Unable to parse '587.82905693530529'
//       ERROR: Unable to parse '562.25210175023039'
//       ERROR: Unable to parse '600.7711019370571'
//       ERROR: Unable to parse '299.46113717717174'
Imports System.Globalization
Imports System.IO
Imports System.Threading

Module Example15
    Public Sub Main15()
        ' Create ten random doubles.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        Dim numbers() As Double = GetRandomNumbers(10)
        DisplayRandomNumbers(numbers)

        ' Persist the numbers as strings.
        Dim sw As New StreamWriter("randoms.dat")
        For ctr As Integer = 0 To numbers.Length - 1
            sw.Write("{0:R}{1}", numbers(ctr), If(ctr < numbers.Length - 1, "|", ""))
        Next
        sw.Close()

        ' Read the persisted data.
        Dim sr As New StreamReader("randoms.dat")
        Dim numericData As String = sr.ReadToEnd()
        sr.Close()
        Dim numberStrings() As String = numericData.Split("|"c)

        ' Restore and display the data using the conventions of the en-US culture.
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        For Each numberStr In numberStrings
            Dim restoredNumber As Double
            If Double.TryParse(numberStr, restoredNumber) Then
                Console.WriteLine(restoredNumber.ToString("R"))
            Else
                Console.WriteLine("ERROR: Unable to parse '{0}'", numberStr)
            End If
        Next
        Console.WriteLine()

        ' Restore and display the data using the conventions of the fr-FR culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR")
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        For Each numberStr In numberStrings
            Dim restoredNumber As Double
            If Double.TryParse(numberStr, restoredNumber) Then
                Console.WriteLine(restoredNumber.ToString("R"))
            Else
                Console.WriteLine("ERROR: Unable to parse '{0}'", numberStr)
            End If
        Next
    End Sub

    Private Function GetRandomNumbers(n As Integer) As Double()
        Dim rnd As New Random()
        Dim numbers(n - 1) As Double
        For ctr As Integer = 0 To n - 1
            numbers(ctr) = rnd.NextDouble * 1000
        Next
        Return numbers
    End Function

    Private Sub DisplayRandomNumbers(numbers As Double())
        For ctr As Integer = 0 To numbers.Length - 1
            Console.WriteLine(numbers(ctr).ToString("R"))
        Next
        Console.WriteLine()
    End Sub
End Module
' The example displays output like the following:
'       487.0313743534644
'       674.12000879371533
'       498.72077885024288
'       42.3034229512808
'       970.57311049223563
'       531.33717716268131
'       587.82905693530529
'       562.25210175023039
'       600.7711019370571
'       299.46113717717174
'       
'       Current Culture: English (United States)
'       487.0313743534644
'       674.12000879371533
'       498.72077885024288
'       42.3034229512808
'       970.57311049223563
'       531.33717716268131
'       587.82905693530529
'       562.25210175023039
'       600.7711019370571
'       299.46113717717174
'       
'       Current Culture: French (France)
'       ERROR: Unable to parse '487.0313743534644'
'       ERROR: Unable to parse '674.12000879371533'
'       ERROR: Unable to parse '498.72077885024288'
'       ERROR: Unable to parse '42.3034229512808'
'       ERROR: Unable to parse '970.57311049223563'
'       ERROR: Unable to parse '531.33717716268131'
'       ERROR: Unable to parse '587.82905693530529'
'       ERROR: Unable to parse '562.25210175023039'
'       ERROR: Unable to parse '600.7711019370571'
'       ERROR: Unable to parse '299.46113717717174'

この問題を回避するには、次の方法のいずれかを使用します。

  • ユーザーのカルチャに関係なく同じであるカスタム書式指定文字列を使用して数値の文字列表現を保存および解析します。
  • CultureInfo.InvariantCulture プロパティによって返されるインバリアント カルチャの書式指定規則を使用して文字列として数値を保存します。

通貨値のシリアル化は特殊なケースです。 通貨値は、それが表現されている通貨の単位に依存するため、独立した数値として扱う意味はほとんどありません。 ただし、通貨値を、通貨記号を含んだ書式設定された文字列として保存する場合、次の例に示すように、既定のカルチャが異なる通貨記号を使用するシステムで逆シリアル化できません。

using System;
using System.Globalization;
using System.IO;
using System.Threading;

public class Example1
{
   public static void Main1()
   {
      // Display the currency value.
      Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
      Decimal value = 16039.47m;
      Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName);
      Console.WriteLine("Currency Value: {0:C2}", value);

      // Persist the currency value as a string.
      StreamWriter sw = new StreamWriter("currency.dat");
      sw.Write(value.ToString("C2"));
      sw.Close();

      // Read the persisted data using the current culture.
      StreamReader sr = new StreamReader("currency.dat");
      string currencyData = sr.ReadToEnd();
      sr.Close();

      // Restore and display the data using the conventions of the current culture.
      Decimal restoredValue;
      if (Decimal.TryParse(currencyData, out restoredValue))
         Console.WriteLine(restoredValue.ToString("C2"));
      else
         Console.WriteLine("ERROR: Unable to parse '{0}'", currencyData);
      Console.WriteLine();

      // Restore and display the data using the conventions of the en-GB culture.
      Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
      Console.WriteLine("Current Culture: {0}",
                        Thread.CurrentThread.CurrentCulture.DisplayName);
      if (Decimal.TryParse(currencyData, NumberStyles.Currency, null, out restoredValue))
         Console.WriteLine(restoredValue.ToString("C2"));
      else
         Console.WriteLine("ERROR: Unable to parse '{0}'", currencyData);
      Console.WriteLine();
   }
}
// The example displays output like the following:
//       Current Culture: English (United States)
//       Currency Value: $16,039.47
//       ERROR: Unable to parse '$16,039.47'
//
//       Current Culture: English (United Kingdom)
//       ERROR: Unable to parse '$16,039.47'
Imports System.Globalization
Imports System.IO
Imports System.Threading

Module Example1
    Public Sub Main1()
        ' Display the currency value.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        Dim value As Decimal = 16039.47D
        Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
        Console.WriteLine("Currency Value: {0:C2}", value)

        ' Persist the currency value as a string.
        Dim sw As New StreamWriter("currency.dat")
        sw.Write(value.ToString("C2"))
        sw.Close()

        ' Read the persisted data using the current culture.
        Dim sr As New StreamReader("currency.dat")
        Dim currencyData As String = sr.ReadToEnd()
        sr.Close()

        ' Restore and display the data using the conventions of the current culture.
        Dim restoredValue As Decimal
        If Decimal.TryParse(currencyData, restoredValue) Then
            Console.WriteLine(restoredValue.ToString("C2"))
        Else
            Console.WriteLine("ERROR: Unable to parse '{0}'", currencyData)
        End If
        Console.WriteLine()

        ' Restore and display the data using the conventions of the en-GB culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        If Decimal.TryParse(currencyData, NumberStyles.Currency, Nothing, restoredValue) Then
            Console.WriteLine(restoredValue.ToString("C2"))
        Else
            Console.WriteLine("ERROR: Unable to parse '{0}'", currencyData)
        End If
        Console.WriteLine()
    End Sub
End Module
' The example displays output like the following:
'       Current Culture: English (United States)
'       Currency Value: $16,039.47
'       ERROR: Unable to parse '$16,039.47'
'       
'       Current Culture: English (United Kingdom)
'       ERROR: Unable to parse '$16,039.47'

代わりに、カルチャの名前など、カルチャに関する情報と共に数値をシリアル化して、値とその通貨記号が現在のカルチャと関係なく逆シリアル化できるようにする必要があります。 次の例では、2 つメンバーを使用して CurrencyValue 構造体を定義することでこれを実行しています。2 つのメンバーは、Decimal 値とその値が属するカルチャの名前です。

using System;
using System.Globalization;
using System.Text.Json;
using System.Threading;

public class Example2
{
    public static void Main2()
    {
        // Display the currency value.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        Decimal value = 16039.47m;
        Console.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.DisplayName}");
        Console.WriteLine($"Currency Value: {value:C2}");

        // Serialize the currency data.
        CurrencyValue data = new()
        {
            Amount = value,
            CultureName = CultureInfo.CurrentCulture.Name
        };
        string serialized = JsonSerializer.Serialize(data);
        Console.WriteLine();

        // Change the current culture.
        CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
        Console.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.DisplayName}");

        // Deserialize the data.
        CurrencyValue restoredData = JsonSerializer.Deserialize<CurrencyValue>(serialized);

        // Display the round-tripped value.
        CultureInfo culture = CultureInfo.CreateSpecificCulture(restoredData.CultureName);
        Console.WriteLine($"Currency Value: {restoredData.Amount.ToString("C2", culture)}");
    }
}

internal struct CurrencyValue
{
    public decimal Amount { get; set; }
    public string CultureName { get; set; }
}

// The example displays the following output:
//       Current Culture: English (United States)
//       Currency Value: $16,039.47
//
//       Current Culture: English (United Kingdom)
//       Currency Value: $16,039.47
Imports System.Globalization
Imports System.Text.Json
Imports System.Threading

Friend Structure CurrencyValue
    Public Property Amount As Decimal
    Public Property CultureName As String
End Structure

Module Example2
    Public Sub Main2()
        ' Display the currency value.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        Dim value As Decimal = 16039.47D
        Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
        Console.WriteLine("Currency Value: {0:C2}", value)

        ' Serialize the currency data.
        Dim data As New CurrencyValue With {
            .Amount = value,
            .CultureName = CultureInfo.CurrentCulture.Name
        }

        Dim serialized As String = JsonSerializer.Serialize(data)
        Console.WriteLine()

        ' Change the current culture.
        CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
        Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)

        ' Deserialize the data.
        Dim restoredData As CurrencyValue = JsonSerializer.Deserialize(Of CurrencyValue)(serialized)

        ' Display the round-tripped value.
        Dim culture As CultureInfo = CultureInfo.CreateSpecificCulture(restoredData.CultureName)
        Console.WriteLine("Currency Value: {0}", restoredData.Amount.ToString("C2", culture))
    End Sub
End Module

' The example displays the following output:
'       Current Culture: English (United States)
'       Currency Value: $16,039.47
'       
'       Current Culture: English (United Kingdom)
'       Currency Value: $16,039.47

カルチャ固有の設定を使用する

.NET では、特定のカルチャまたは地域は CultureInfo クラスによって表されます。 そのプロパティの一部は、カルチャのある側面に関する特定の情報を提供するオブジェクトを返します。

  • CultureInfo.CompareInfo プロパティは、カルチャによる文字列の比較方法および並べ替え方法に関する情報を含んだ CompareInfo オブジェクトを返します。

  • CultureInfo.DateTimeFormat プロパティは、日付と時刻のデータの書式設定で使用されるカルチャ固有の情報を提供する DateTimeFormatInfo オブジェクトを返します。

  • CultureInfo.NumberFormat プロパティは、数値データの書式設定で使用されるカルチャ固有の情報を提供する NumberFormatInfo オブジェクトを返します。

  • CultureInfo.TextInfo プロパティは、カルチャの書記体系に関する情報を提供する TextInfo オブジェクトを返します。

一般に、特定の CultureInfo プロパティおよび関連するオブジェクトの値について何も想定しないでください。 代わりに、次の理由により、カルチャ固有のデータは変更される可能性があると考える必要があります。

  • 個々のプロパティ値は、データが修正された、より優れたデータが使用可能になった、カルチャ固有の規則が変更されたなどの理由で、時間と共に変更または修正される可能性があります。

  • 個々のプロパティ値は、.NET のバージョンまたはオペレーティング システムのバージョン間で異なる場合があります。

  • .NET では置換カルチャをサポートしています。 これにより、既存の標準カルチャを補足または既存の標準カルチャを完全に置き換える新しいカスタム カルチャを定義できます。

  • Windows システムのユーザーは、コントロール パネルの [地域と言語] アプリを使用してカルチャ固有の設定をカスタマイズできます。 CultureInfo オブジェクトをインスタンス化するときに、CultureInfo(String, Boolean) コンストラクターを呼び出すことでこのようなユーザーによるカスタマイズを反映するかどうかを決定できます。 通常は、エンド ユーザーのアプリでは、ユーザー設定を尊重して、ユーザー自身が予期する形式でデータを表示する必要があります。

関連項目