Globalisation

La globalisation a trait à la conception et au développement d’applications universelles qui prennent en charge les interfaces localisées et les données régionales pour les utilisateurs de diverses cultures. Avant d’entreprendre la phase de conception, vous devez déterminer les cultures que votre application devra prendre en charge. Bien qu’une application cible par défaut une seule et même culture ou région, vous pouvez la concevoir et l’écrire de façon à faciliter son extension à d’autres cultures ou régions.

En tant que développeurs, nous avons tous des principes en matière d’interfaces utilisateur et des données qui sont formées par nos cultures. Par exemple, pour un développeur dont la langue est l’anglais des États-Unis, sérialiser les données de date et d’heure sous forme de chaîne au format MM/dd/yyyy hh:mm:ss semble tout à fait raisonnable. Cependant, la désérialisation de cette chaîne sur un système d’une autre culture est susceptible de lever une exception FormatException ou de produire des données incorrectes. La globalisation nous permet d’identifier ces principes propres aux cultures et de s’assurer qu’elles n’affectent pas la conception ou le code de notre application.

Cet article expose certaines des questions phares que vous devez prendre en considération, ainsi que les bonnes pratiques que vous pouvez suivre pour gérer les chaînes, les valeurs de date et d’heure et les valeurs numériques dans une application globalisée.

Chaînes

La gestion des caractères et des chaînes est un élément central de la globalisation, car chaque culture ou région peut utiliser des caractères et des jeux de caractères différents et les trier différemment. Cette section émet des recommandations pour l’utilisation de chaînes dans les applications globalisées.

Utiliser Unicode en interne

Par défaut, .NET utilise des chaînes Unicode. Une chaîne Unicode se compose de zéro, un ou plusieurs objets Char, chacun représentant une unité de code UTF-16. Il existe une représentation Unicode pour pratiquement tous les caractères de chaque jeu de caractères utilisé dans le monde.

Beaucoup d’applications et de systèmes d’exploitation, dont le système d’exploitation Windows, peuvent aussi utiliser des pages de codes pour représenter des jeux de caractères. Les pages de codes contiennent généralement les valeurs ASCII standard (de 0x00 à 0x7F) et mappent les autres caractères aux valeurs restantes (de 0x80 à 0xFF). L’interprétation des valeurs 0x80 à 0xFF dépend de la page de codes utilisée. C’est pour cette raison que vous devez éviter d’utiliser des pages de codes dans une application globalisée, dans la mesure du possible.

L’exemple suivant illustre les risques liés à l’interprétation des données de page de codes quand la page de codes par défaut d’un système est différente de celle sur lequel les données ont été enregistrées. (Pour simuler ce scénario, l’exemple spécifie explicitement des pages de codes différentes.) Tout d’abord, l’exemple définit un tableau qui se compose des caractères majuscules de l’alphabet grec. Ces caractères sont encodés dans un tableau d’octets en utilisant la page de codes 737 (aussi appelée MS-DOS Grec), tableau d’octets qui est ensuite enregistré dans un fichier. Si le fichier est récupéré et que son tableau d’octets est décodé en utilisant la page de codes 737, les caractères d’origine sont restaurés. En revanche, si le fichier est récupéré et que son tableau d’octets est décodé en utilisant la page de codes 1252 (ou Windows-1252, qui représente les caractères de l’alphabet latin), les caractères d’origine sont perdus.

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:
'       ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ
'       €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’""•–—

L’utilisation d’Unicode est la garantie que les mêmes unités de code correspondent toujours aux mêmes caractères, et que les mêmes caractères correspondent toujours aux mêmes tableaux d’octets.

Utiliser les fichiers de ressources

Même si vous décidez de développer une application qui cible une seule et même culture ou région, vous avez tout intérêt à utiliser des fichiers de ressources pour y stocker les chaînes et les autres ressources qui s’afficheront dans l’interface utilisateur. Vous ne devez jamais les ajouter directement à votre code. L’utilisation de fichiers de ressources offre un certain nombre d’avantages :

  • Toutes les chaînes se trouvent à un même emplacement. Vous n’avez pas besoin de parcourir tout le code source pour identifier les chaînes à modifier pour une langue ou une culture donnée.
  • Il n’est pas nécessaire de dupliquer les chaînes. Souvent, les développeurs qui n’utilisent pas de fichiers de ressources définissent une même chaîne dans plusieurs fichiers de code source. Or, cette duplication augmente les chances d’oubli d’une ou plusieurs instances d’une chaîne modifiée.
  • Au lieu de stocker les ressources qui ne sont pas des chaînes (p.ex., images ou données binaires) dans un fichier autonome, vous pouvez les inclure dans le fichier de ressources de façon à en faciliter la récupération.

L’utilisation de fichiers de ressources offre des avantages particuliers si vous créez une application localisée. Quand vous déployez des ressources dans des assemblys satellites, le common language runtime sélectionne automatiquement la ressource adaptée à la culture en fonction de la culture d’interface utilisateur active de l’utilisateur définie par la propriété CultureInfo.CurrentUICulture. Dans la mesure où vous fournissez une ressource appropriée propre à la culture et que vous instanciez correctement un objet ResourceManager ou que vous utilisez une classe de ressource fortement typée, le runtime se charge de récupérer les ressources appropriées.

Pour plus d’informations sur la création de fichiers de ressources, consultez Création de fichiers de ressources. Pour plus d’informations sur la création et le déploiement d’assemblys satellites, consultez Créer des assemblys satellites et Empaqueter et déployer des ressources.

Rechercher et comparer des chaînes

Dans la mesure du possible, vous devez traiter les chaînes comme des chaînes entières, et non comme une série de caractères individuels. Cela est particulièrement important quand il s’agit de trier ou rechercher des sous-chaînes de façon à éviter les problèmes liés à l’analyse de caractères combinés.

Conseil

Vous pouvez utiliser la classe StringInfo pour travailler sur les éléments de texte plutôt que sur les caractères individuels d’une chaîne.

Pendant les opérations de recherche et de comparaison de chaînes, l’erreur courante à éviter est de traiter les chaînes comme des ensembles de caractères, chacun représenté par un objet Char. De fait, un même caractère peut être constitué d’un ou plusieurs objets Char. Ces caractères se trouvent généralement dans des chaînes de cultures dont les alphabets comportent des caractères extérieurs à la plage de caractères latins de base Unicode (de U+0021 à U+007E). L’exemple suivant recherche l’index du caractère LETTRE MAJUSCULE LATINE A AVEC ACCENT GRAVE (U+00C 0) dans une chaîne. Or, ce caractère peut être représenté de deux façons différentes : soit en tant qu’unité de code unique (U+00C0), soit en tant que caractère composite (deux unités de code : U+0041 et U+0300). Dans ce cas, le caractère est représenté dans l’instance de la chaîne par deux objets Char, U+0041 et U+0300. L’exemple de code appelle les surcharges String.IndexOf(Char) et String.IndexOf(String) pour rechercher la position de ce caractère dans l’instance de la chaîne, mais elles retournent des résultats différents. Le premier appel de méthode comporte un argument Char ; il effectue une comparaison ordinale et ne peut donc pas trouver de correspondance. Le deuxième appel comporte un argument String ; il effectue une comparaison dépendante de la culture et trouve donc une correspondance.

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

Vous pouvez lever une partie de l’ambiguïté de cet exemple (appel à deux surcharges similaires d’une méthode qui retourne des résultats différents) en appelant une surcharge qui inclue un paramètre StringComparison, tel que la méthode String.IndexOf(String, StringComparison) ou String.LastIndexOf(String, StringComparison).

Cependant, les recherches ne sont pas toujours dépendantes de la culture. Si la recherche vise à prendre une décision en matière de sécurité ou à autoriser ou interdire l’accès à une ressource, la comparaison doit être ordinale, comme nous le verrons dans la section suivante.

Tester l’égalité de chaînes

Si vous voulez effectuer un test d’égalité entre deux chaînes au lieu de les comparer par rapport à l’ordre de tri, utilisez la méthode String.Equals plutôt qu’une méthode de comparaison de chaînes comme String.Compare ou CompareInfo.Compare.

Les comparaisons d’égalité sont généralement effectuées pour accéder de manière conditionnelle à certaines ressources. Par exemple, vous pouvez effectuer une comparaison d’égalité pour vérifier un mot de passe ou confirmer qu’un fichier existe. Ces comparaisons non linguistiques doivent toujours être ordinales et non dépendantes de la culture. En général, vous avez tout intérêt à appeler la méthode d’instance String.Equals(String, StringComparison) ou la méthode statique String.Equals(String, String, StringComparison) avec la valeur StringComparison.Ordinal pour des chaînes telles que des mots de passe et la valeur StringComparison.OrdinalIgnoreCase pour des chaînes telles que des noms de fichiers ou des URI.

Les comparaisons d’égalité impliquent parfois des recherches ou des comparaisons de sous-chaînes plutôt que des appels à la méthode String.Equals. Dans certains cas, vous pouvez rechercher une sous-chaîne pour déterminer si elle est égale à une autre chaîne. Si l’objet de cette comparaison n’est pas linguistique, la recherche doit aussi être ordinale et non dépendante de la culture.

L’exemple suivant illustre le risque associé à une recherche dépendante de la culture sur des données non linguistiques. La méthode AccessesFileSystem vise à interdire l’accès au système de fichiers pour les URI commençant par la sous-chaîne « FILE». Pour cela, elle effectue une comparaison dépendante de la culture et sans respect de la casse entre le début de l’URI et la chaîne « FILE ». Sachant qu’un URI qui accède au système de fichiers peut commencer par « FILE: » ou « file: », la supposition implicite est que ce « i » (U+0069) est toujours l’équivalent en minuscule de « I » (U+0049). Toutefois, en turc et en azéri, la version en majuscule du « i » est « i » (U + 0130). Du fait de cette différence, la comparaison dépendante de la culture autorise l’accès au système de fichiers, alors qu’il devrait être interdit.

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.

Vous pouvez éviter ce problème en effectuant une comparaison ordinale qui ignore la casse, comme le montre l’exemple suivant.

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.

Ordonner et trier des chaînes

En règle générale, les chaînes ordonnées devant s’afficher dans l’interface utilisateur doivent être triées en fonction de la culture. La plupart du temps, ces comparaisons de chaînes sont gérées implicitement par .NET dès lors que vous appelez une méthode qui trie les chaînes, comme Array.Sort ou List<T>.Sort. Par défaut, les chaînes sont triées selon les conventions de tri de la culture active. L’exemple suivant illustre la différence entre le tri d’un tableau de chaînes avec les conventions de la culture Anglais (États-Unis) et le tri de ce même tableau avec la culture Suédois (Suède).

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

La comparaison de chaînes dépendante de la culture est définie par l’objet CompareInfo, qui est retourné par la propriété CultureInfo.CompareInfo de chaque culture. Les comparaisons de chaînes dépendantes de la culture qui utilisent les surcharges de méthode String.Compare utilisent aussi l’objet CompareInfo.

.NET utilise des tables pour effectuer des tris dépendants de la culture sur les données de chaîne. Le contenu de ces tables, qui comportent des données de pondération de tri et de normalisation de chaînes, est déterminé par la version de la norme Unicode implémentée par une version déterminée de .NET. Le tableau suivant répertorie les versions d’Unicode implémentées par les versions indiquées de .NET. Cette liste de versions d’Unicode prises en charge s’applique uniquement à la comparaison et au tri de caractères ; elle ne s’applique pas à la classification des caractères Unicode par catégorie. Pour plus d’informations, consultez la section « Chaînes et norme Unicode » de l’article String.

Version du .NET Framework Système d’exploitation Version d’Unicode
.NET Framework 2.0 Tous les systèmes d’exploitation Unicode 4.1
.NET Framework 3.0 Tous les systèmes d’exploitation Unicode 4.1
.NET Framework 3.5 Tous les systèmes d’exploitation Unicode 4.1
.NET Framework 4 Tous les systèmes d’exploitation Unicode 5.0
.NET Framework 4.5 et ultérieur Windows 7 Unicode 5.0
.NET Framework 4.5 et ultérieur Windows 8 et les systèmes d'exploitation ultérieurs Unicode 6.3.0
.NET Core et .NET 5+ En fonction de la version de la norme Unicode prise en charge par le SE sous-jacent.

Depuis .NET Framework 4.5 et dans toutes les versions de .NET Core et .NET 5+, la comparaison et le tri de chaînes dépendent du système d’exploitation. .NET Framework 4.5 et les versions ultérieures exécutées sur Windows 7 récupèrent les données dans leurs propres tables qui implémentent Unicode 5.0. .NET Framework 4.5 et les versions ultérieures exécutées sur Windows 8 récupèrent les données dans leurs propres tables qui implémentent Unicode 6.3. Sur .NET Core et .NET 5+, la version prise en charge d’Unicode dépend du système d’exploitation sous-jacent. Si vous sérialisez des données triées dépendantes de la culture, vous pouvez utiliser la classe SortVersion pour déterminer à quel moment vos données sérialisées doivent être triées de sorte qu’elles correspondent au .NET et à l’ordre de tri du système d’exploitation. Pour obtenir un exemple, consultez la rubrique relative à la classe SortVersion.

Si votre application effectue des tris étendus propres à la culture sur des données de chaîne, vous pouvez utiliser la classe SortKey pour comparer des chaînes. Une clé de tri reflète les pondérations de tri propres à la culture, notamment les pondérations alphabétiques, de casse et diacritiques d’une chaîne déterminée. Les comparaisons à base de clés de tri étant binaires, elles sont plus rapides que les comparaisons qui utilisent un objet CompareInfo, que ce soit de manière implicite ou explicite. Vous pouvez créer une clé de tri propre à la culture pour une chaîne déterminée en passant la chaîne à la méthode CompareInfo.GetSortKey.

L’exemple suivant est similaire à l’exemple précédent. Cependant, au lieu d’appeler la méthode Array.Sort(Array), qui appelle implicitement la méthode CompareInfo.Compare, il définit une implémentation System.Collections.Generic.IComparer<T> qui compare les clés de tri, qu’il instancie et passe à la méthode 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

Éviter la concaténation de chaînes

Dans la mesure du possible, évitez d’utiliser les chaînes composites qui sont construites au moment de l’exécution à partir d’expressions concaténées. Les chaînes composites sont difficiles à localiser, car elles prennent souvent en compte l’ordre grammatical de la langue d’origine de l’application qui ne s’applique pas aux autres langues localisées.

Gérer des dates et des heures

La façon dont vous gérez les valeurs de date et d’heure varie selon qu’elles s’affichent dans l’interface utilisateur ou qu’elles sont persistantes. Cette section se penche sur les deux cas. De même, elle explique comment gérer les décalages horaires et les opérations arithmétiques appliquées aux dates et heures.

Afficher des dates et des heures

En règle générale, quand les dates et les heures s’affichent dans l’interface utilisateur, vous devez utiliser les conventions de mise en forme de la culture de l’utilisateur, ce qui est défini par la propriété CultureInfo.CurrentCulture et l’objet DateTimeFormatInfo retourné par la propriété CultureInfo.CurrentCulture.DateTimeFormat. Les conventions de mise en forme de la culture active sont utilisées automatiquement quand vous mettez en forme une date en utilisant l’une de ces méthodes :

L’exemple suivant affiche des données relatives au lever du soleil (« sunrise ») et au coucher du soleil (« sunset ») à deux reprises pour le 11 octobre 2012. Dans un premier temps, il définit la culture active Croate (Croatie), puis Anglais (Royaume-Uni). Dans les deux cas, les dates et heures s’affichent au format adapté à la culture en question.

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

Figer des dates et des heures

Vous ne devez jamais figer les données de date et d’heure dans un format qui peut varier d’une culture à une autre. Il s’agit d’une erreur de programmation courante qui se traduit par des données endommagées ou par une exception au moment de l’exécution. L’exemple suivant sérialise deux dates (le 9 janvier 2013 et le 18 août 2013) sous forme de chaînes en utilisant les conventions de mise en forme de la culture Anglais (États-Unis). Quand les données sont récupérées et analysées en utilisant les conventions de la culture Anglais (États-Unis), elles sont restaurées avec succès. En revanche, quand elles sont récupérées et analysées en utilisant les conventions de la culture Anglais (Royaume-Uni), la première date est interprétée par erreur comme étant le 1er septembre, et la deuxième ne peut pas être analysée, car le calendrier grégorien n’a pas de dix-huitième mois.

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

Vous pouvez éviter ce problème de l’une des trois façons suivantes :

  • Sérialisez la date et l’heure au format binaire et non sous forme de chaîne.
  • Enregistrez et analysez la représentation de la date et de l’heure sous la forme d’une chaîne de format personnalisé qui reste identique, quelle que soit la culture de l’utilisateur.
  • Enregistrez la chaîne en utilisant les conventions de mise en forme de la culture dite indifférente.

L’exemple suivant illustre la dernière approche. Il utilise les conventions de mise en forme de la culture dite indifférente retournée par la propriété statique 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

Prise en compte de la sérialisation et des fuseaux horaires

Une valeur de date et d’heure peut être interprétée de plusieurs façons, par exemple, en tant qu’heure générale (« les magasins seront ouverts le 2 janvier 2013 à 9h00 ») ou en tant que moment précis dans le temps (« Date de naissance : le 2 janvier 2013 à 6h32 »). Quand une valeur d’heure représente un moment précis dans le temps et que vous la restaurez à partir d’une valeur sérialisée, vous devez vérifier qu’elle représente le même moment dans le temps, quelle que soit la situation géographique ou le fuseau horaire de l’utilisateur.

L'exemple de code suivant illustre ce problème. Il enregistre une valeur de date et d’heure locale unique en tant que chaîne dans trois formats standard :

  • « G » pour une date générale (heure longue).
  • « s » pour une date/heure triable.
  • « o » pour une date/heure aller-retour.
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

Quand les données sont restaurées sur un système situé dans le même fuseau horaire que le système sur lequel elles ont été sérialisées, les valeurs de date et d’heure désérialisées sont le reflet exact de la valeur d’origine, comme le montre la sortie suivante :

'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

Cependant, si vous restaurez les données sur un système situé dans un autre fuseau horaire, seule la valeur de date et d’heure qui a été mise en forme avec la chaîne de format standard « o » (aller-retour) conserve les informations de fuseau horaire et par conséquent représente le même instant dans le temps. Voici le résultat de la restauration des données de date et d’heure sur un système situé dans le fuseau horaire d’Europe centrale :

'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

Pour représenter avec précision une valeur de date et d’heure qui correspond à un moment précis dans le temps, indépendamment du fuseau horaire du système sur lequel les données sont désérialisées, vous pouvez procéder de l’une des façons suivantes :

  • Enregistrez la valeur sous forme de chaîne en utilisant la chaîne de format « o » (aller-retour). Ensuite, désérialisez-la sur le système cible.
  • Convertissez-la au format UTC et enregistrez-la sous forme de chaîne en utilisant la chaîne de format standard « r » (RFC1123). Désérialisez-la ensuite sur le système cible et convertissez-la en heure locale.
  • Convertissez-la au format UTC et enregistrez-la sous forme de chaîne en utilisant la chaîne de format standard « u » (universel pouvant être trié). Désérialisez-la ensuite sur le système cible et convertissez-la en heure locale.

L’exemple suivant illustre chaque technique.

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

Quand les données sont sérialisées sur un système situé dans le fuseau horaire du Pacifique, puis désérialisées sur un système situé dans le fuseau horaire d’Europe centrale, l’exemple affiche la sortie suivante :

'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

Pour plus d’informations, consultez Convertir des heures entre des fuseaux horaires.

Exécuter des calculs de date et d’heure

Les types DateTime et DateTimeOffset prennent en charge les opérations arithmétiques. Vous pouvez calculer la différence entre deux valeurs de date, ou vous pouvez ajouter ou soustraire des intervalles de temps déterminés à une valeur de date. Cependant, les opérations arithmétiques sur les valeurs de date et d’heure ne prennent pas en compte les fuseaux horaires et les règles d’ajustement de fuseaux horaires. Pour cette raison, les calculs arithmétiques sur les valeurs de date et d’heure qui représentent des moments dans le temps peuvent retourner des résultats incorrects.

Par exemple, le passage de l’heure d’hiver à l’heure d’été dans la zone Pacifique intervient le deuxième dimanche de mars, c’est-à-dire le 10 mars pour l’année 2013. Comme le montre l’exemple suivant, si vous calculez la date et l’heure à 48 heures après le 9 mars 2013 à 10h30 sur un système situé dans le fuseau horaire du Pacifique, le résultat (11 mars 2013 à 10h30) ne prend pas en compte le changement d’heure intervenu entre-temps.

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

Pour être certain qu’une opération arithmétique sur des valeurs de date et d’heure génère des résultats précis, procédez comme suit :

  1. Convertissez l’heure du fuseau horaire source au format UTC.
  2. Effectuez l’opération arithmétique.
  3. Si le résultat obtenu est une valeur de date et d’heure, convertissez-la du format UTC à l’heure du fuseau horaire source.

L’exemple suivant est similaire à l’exemple précédent, sauf qu’il respecte ces trois étapes pour ajouter correctement 48 heures au 9 mars 2013 à 10h30.

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

Pour plus d’informations, consultez Effectuer des opérations arithmétiques avec des dates et des heures.

Utiliser des noms dépendants de la culture pour les éléments de date

Votre application peut avoir besoin d’afficher le nom du mois ou le jour de la semaine. Pour cela, le code suivant est d’usage courant.

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.

Or, ce code retourne toujours le nom des jours de la semaine en anglais. Il est fréquent que le code chargé d’extraire le nom du mois soit encore moins flexible. Il part souvent du principe que le calendrier est constitué de douze mois et affiche le nom des mois dans une langue spécifique.

En utilisant des chaînes de format de date et d’heure personnalisées ou les propriétés de l’objet DateTimeFormatInfo, il est facile d’extraire des chaînes qui correspondent au nom des jours de la semaine ou des mois dans la culture de l’utilisateur, comme l’illustre l’exemple suivant. Il remplace la culture active par la culture Français (France) et affiche le nom du jour de la semaine et le nom du mois pour le 1er juillet 2013.

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.

Valeurs numériques

La gestion des nombres varie selon qu’ils s’affichent dans l’interface utilisateur ou qu’ils sont persistants. Cette section se penche sur les deux cas.

Notes

Pendant les opérations d’analyse et de mise en forme, le .NET ne reconnaît comme caractères numériques que les caractères latins de base compris entre 0 à 9 (de U+0030 à U+0039).

Afficher des valeurs numériques

En règle générale, quand les nombres s’affichent dans l’interface utilisateur, vous devez utiliser les conventions de mise en forme de la culture de l’utilisateur, ce qui est défini par la propriété CultureInfo.CurrentCulture et l’objet NumberFormatInfo retourné par la propriété CultureInfo.CurrentCulture.NumberFormat. Les conventions de mise en forme de la culture active sont utilisées automatiquement quand vous mettez en forme une date de l’une des manières suivantes :

  • Utilisation de la méthode ToString sans paramètre de tout type numérique.
  • Utilisation de la méthode ToString(String) de tout type numérique, qui inclut une chaîne de format en tant d’argument.
  • Utilisation de la mise en forme composite avec des valeurs numériques.

L’exemple suivant affiche la température moyenne par mois à Paris. Dans un premier temps, il définit la culture active Français (France) avant d’afficher les données, puis définit ensuite Anglais (États-Unis). Dans les deux cas, le nom des mois et les températures s’affichent au format adapté à la culture en question. À noter que les deux cultures utilisent des séparateurs décimaux différents dans la valeur de température. De même, notez que la chaîne de format de date et d’heure personnalisée « MMMM » est utilisée pour afficher le nom de mois complet et que la quantité d’espace nécessaire est allouée au nom de mois dans la chaîne de résultat par rapport à la longueur du nom de mois le plus long dans la tableau 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

Conserver des valeurs numériques

Vous ne devez jamais conserver les données numériques dans un format propre à la culture. Il s’agit d’une erreur de programmation courante qui se traduit par des données endommagées ou par une exception au moment de l’exécution. L’exemple suivant génère dix nombres à virgule flottante aléatoires et les sérialise sous forme de chaînes en utilisant les conventions de mise en forme de la culture Anglais (États-Unis). Quand les données sont récupérées et analysées en utilisant les conventions de la culture Anglais (États-Unis), elles sont restaurées avec succès. Cependant, quand elles sont récupérées et analysées en utilisant les conventions de la culture Français (France), aucun de ces nombres ne peut être analysé, car les cultures utilisent des séparateurs décimaux différents.

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'

Pour éviter ce problème, vous pouvez utiliser l’une de ces techniques :

  • Enregistrez et analysez la représentation du nombre sous la forme d’une chaîne de format personnalisé qui reste identique, quelle que soit la culture de l’utilisateur.
  • Enregistrez le nombre sous forme de chaîne en utilisant les conventions de mise en forme de la culture dite indifférente, qui est retourné par la propriété CultureInfo.InvariantCulture.

La sérialisation des valeurs de devise est un cas spécial. Sachant qu’une valeur monétaire dépend de l’unité de devise dans laquelle elle est exprimée, il est peu judicieux de la traiter comme une valeur numérique indépendante. En revanche, si vous enregistrez une valeur de devise sous forme de chaîne mise en forme avec un symbole de devise, elle ne peut pas être désérialisée sur un système dont la culture par défaut utilise un autre symbole monétaire, comme le montre l’exemple suivant.

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'

Au lieu de cela, vous devez sérialiser la valeur numérique avec des informations sur la culture, telles que le nom de celle-ci, pour permettre la désérialisation de la valeur et de son symbole de devise indépendamment de la culture active. C’est ce que fait l’exemple suivant en définissant une structure CurrencyValue avec deux membres : la valeur Decimal et le nom de la culture à laquelle appartient la valeur.

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

Utiliser des paramètres spécifiques à la culture

Dans .NET, la classe CultureInfo représente une culture ou une région particulière. Certaines de ses propriétés retournent des objets qui fournissent des informations spécifiques sur un aspect déterminé d’une culture :

En général, ne faites aucune supposition quant aux valeurs des propriétés CultureInfo spécifiques et des objets associés. Vous devez plutôt considérer les données propres à la culture comme des valeurs susceptibles de changer pour les raisons suivantes :

  • Les valeurs des propriété individuelles sont susceptibles de changer et d’évoluer dans le temps, à mesure que les données sont corrigées, que des données plus fiables sont disponibles ou que les conventions propres à la culture évoluent.

  • Les valeurs des propriétés individuelles peuvent varier entre les différentes versions du .NET ou du système d’exploitation.

  • .NET prend en charge les cultures de remplacement. Cela permet de définir une nouvelle culture personnalisée qui complète les cultures standard existants ou qui remplace entièrement une culture standard existante.

  • Sur des systèmes Windows, l’utilisateur peut personnaliser les paramètres propres à une culture à l’aide de l’application Région et langue du Panneau de configuration. Quand vous instanciez un objet CultureInfo, vous pouvez déterminer s’il reflète ces personnalisations utilisateur en appelant le constructeur CultureInfo(String, Boolean). En règle générale, pour les applications destinées aux utilisateurs finals, veillez à respecter les préférences de mise en forme des utilisateurs de sorte que les données leur soient présentées comme attendu.

Voir aussi