Export (0) Print
Expand All

Globalization

Globalization involves designing and developing a world-ready app that supports localized interfaces and regional data for users in multiple cultures. Before beginning the design phase, you should determine which cultures your app will support. Although an app targets a single culture or region as its default, you can design and write it so that it can easily be extended to users in other cultures or regions.

As developers, we all have assumptions about user interfaces and data that are formed by our cultures. For example, for an English-speaking developer in the United States, serializing date and time data as a string in the format MM/dd/yyyy hh:mm:ss seems perfectly reasonable. However, deserializing that string on a system in a different culture is likely to throw a FormatException exception or produce inaccurate data. Globalization enables us to identify such culture-specific assumptions and ensure that they do not affect our app's design or code.

The following sections discuss some of the major issues you should consider and the best practices you can follow when handling strings, date and time values, and numeric values in a globalized app.

The handling of characters and strings is a central focus of globalization, because each culture or region may use different characters and character sets and sort them differently. This section provides recommendations for using strings in globalized apps.

By default, the .NET Framework uses Unicode strings. A Unicode string consists of zero, one, or more Char objects, each of which represents a UTF-16 code unit. There is a Unicode representation for almost every character in every character set in use throughout the world.

Many applications and operating systems, including the Windows operating system, can use also use code pages to represent character sets. Code pages typically contain the standard ASCII values from 0x00 through 0x7F and map other characters to the remaining values from 0x80 through 0xFF. The interpretation of values from 0x80 through 0xFF depends on the specific code page. Because of this, you should avoid using code pages in a globalized app if possible.

The following example illustrates the dangers of interpreting code page data when the default code page on a system is different from the code page on which the data was saved. (To simulate this scenario, the example explicitly specifies different code pages.) First, the example defines an array that consists of the uppercase characters of the Greek alphabet. It encodes them into a byte array by using code page 737 (also known as MS-DOS Greek) and saves the byte array to a file. If the file is retrieved and its byte array is decoded by using code page 737, the original characters are restored. However, if the file is retrieved and its byte array is decoded by using code page 1252 (or Windows-1252, which represents characters in the Latin alphabet), the original characters are lost.

Imports System.IO
Imports System.Text

Module Example
   Public Sub Main()
      ' 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 }

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

The use of Unicode ensures that the same code units always map to the same characters, and that the same characters always map to the same byte arrays.

Even if you are developing an app that targets a single culture or region, you should use resource files to store strings and other resources that are displayed in the user interface. You should never add them directly to your code. Using resource files has a number of advantages:

  • All the strings are in a single location. You don't have to search throughout your source code to identify strings to modify for a specific language or culture.

  • There is no need to duplicate strings. Developers who don't use resource files often define the same string in multiple source code files. This duplication increases the probability that one or more instances will be overlooked when a string is modified.

  • You can include non-string resources, such as images or binary data, in the resource file instead of storing them in a separate standalone file, so they can be retrieved easily.

Using resource files has particular advantages if you are creating a localized app. When you deploy resources in satellite assemblies, the common language runtime automatically selects a culture-appropriate resource based on the user's current UI culture as defined by the CultureInfo.CurrentUICulture property. As long as you provide an appropriate culture-specific resource and correctly instantiate a ResourceManager object or use a strongly typed resource class, the runtime handles the details of retrieving the appropriate resources.

For more information about creating resource files, see Creating Resource Files for Desktop Apps. For information about creating and deploying satellite assemblies, see Creating Satellite Assemblies for Desktop Apps and Packaging and Deploying Resources in Desktop Apps.

Whenever possible, you should handle strings as entire strings instead of handling them as a series of individual characters. This is especially important when you sort or search for substrings, to prevent problems associated with parsing combined characters.

Tip Tip

You can use the StringInfo class to work with the text elements rather than the individual characters in a string.

In string searches and comparisons, a common mistake is to treat the string as a collection of characters, each of which is represented by a Char object. In fact, a single character may be formed by one, two, or more Char objects. Such characters are found most frequently in strings from cultures whose alphabets consist of characters outside the Unicode Basic Latin character range (U+0021 through U+007E). The following example tries to find the index of the LATIN CAPITAL LETTER A WITH GRAVE character (U+00C0) in a string. However, this character can be represented in two different ways: as a single code unit (U+00C0) or as a composite character (two code units: U+0021 and U+007E). In this case, the character is represented in the string instance by two Char objects, U+0021 and U+007E. The example code calls the String.IndexOf(Char) and String.IndexOf(String) overloads to find the position of this character in the string instance, but these return different results. The first method call has a Char argument; it performs an ordinal comparison and therefore cannot find a match. The second call has a String argument; it performs a culture-sensitive comparison and therefore finds a match.

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

public class Example
{
   public static void Main()
   {
      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

You can avoid some of the ambiguity of this example (calls to two similar overloads of a method returning different results) by calling an overload that includes a StringComparison parameter, such as the String.IndexOf(String, StringComparison) or String.LastIndexOf(String, StringComparison) method.

However, searches are not always culture-sensitive. If the purpose of the search is to make a security decision or to allow or disallow access to some resource, the comparison should be ordinal, as discussed in the next section.

If you want to test two strings for equality rather than determining how they compare in the sort order, use the String.Equals method instead of a string comparison method such as String.Compare or CompareInfo.Compare.

Comparisons for equality are typically performed to access some resource conditionally. For example, you might perform a comparison for equality to verify a password or to confirm that a file exists. Such non-linguistic comparisons should always be ordinal rather than culture-sensitive. In general, you should call the instance String.Equals(String, StringComparison) method or the static String.Equals(String, String, StringComparison) method with a value of StringComparison.Ordinal for strings such as passwords, and a value of StringComparison.OrdinalIgnoreCase for strings such as file names or URIs.

Comparisons for equality sometimes involve searches or substring comparisons rather than calls to the String.Equals method. In some cases, you may use a substring search to determine whether that substring equals another string. If the purpose of this comparison is non-linguistic, the search should also be ordinal rather than culture-sensitive.

The following example illustrates the danger of a culture-sensitive search on non-linguistic data. The AccessesFileSystem method is designed to prohibit file system access for URIs that begin with the substring "FILE". To do this, it performs a culture-sensitive, case-insensitive comparison of the beginning of the URI with the string "FILE". Because a URI that accesses the file system can begin with either "FILE:" or "file:", the implicit assumption is that that "i" (U+0069) is always the lowercase equivalent of "I" (U+0049). However, in Turkish and Azerbaijani, the uppercase version of "i" is "İ" (U+0130). Because of this discrepancy, the culture-sensitive comparison allows file system access when it should be prohibited.

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

public class Example
{
   public static void Main()
   {
      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.

You can avoid this problem by performing an ordinal comparison that ignores case, as the following example shows.

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

public class Example
{
   public static void Main()
   {
      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.

Typically, ordered strings that are to be displayed in the user interface should be sorted based on culture. For the most part, such string comparisons are handled implicitly by the .NET Framework when you call a method that sorts strings, such as Array.Sort or List<T>.Sort. By default, strings are sorted by using the sorting conventions of the current culture. The following example illustrates the difference when an array of strings is sorted by using the conventions of the English (United States) culture and the Swedish (Sweden) culture.

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

public class Example
{
   public static void Main()
   {
      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

Culture-sensitive string comparison is defined by the CompareInfo object, which is returned by each culture's CultureInfo.CompareInfo property. Culture-sensitive string comparisons that use the String.Compare method overloads also use the CompareInfo object.

The .NET Framework uses tables to perform culture-sensitive sorts on string data. The content of these tables, which contain data on sort weights and string normalization, is determined by the version of the Unicode standard implemented by a particular version of the .NET Framework. The following table lists the versions of Unicode implemented by the specified versions of the .NET Framework.

.NET Framework version

Operating system

Unicode version

.NET Framework 2.0

All operating systems

Unicode 4.1

.NET Framework 3.0

All operating systems

Unicode 4.1

.NET Framework 3.5

All operating systems

Unicode 4.1

.NET Framework 4

All operating systems

Unicode 5.0

.NET Framework 4.5

Windows 7

Unicode 5.0

.NET Framework 4.5

Windows 8

Unicode 6.0

In the .NET Framework 4.5, string comparison and sorting depends on the operating system. The .NET Framework 4.5 running on Windows 7 retrieves data from its own tables that implement Unicode 5.0. The .NET Framework 4.5 running on Windows 8 retrieves data from operating system tables that implement Unicode 6.0. If you serialize culture-sensitive sorted data, you can use the SortVersion class to determine when your serialized data needs to be sorted so that it is consistent with the .NET Framework and the operating system's sort order. For an example, see the SortVersion class topic.

If your app performs extensive culture-specific sorts of string data, you can work with the SortKey class to compare strings. A sort key reflects the culture-specific sort weights, including the alphabetic, case, and diacritic weights of a particular string. Because comparisons using sort keys are binary, they are faster than comparisons that use a CompareInfo object either implicitly or explicitly. You create a culture-specific sort key for a particular string by passing the string to the CompareInfo.GetSortKey method.

The following example is similar to the previous example. However, instead of calling the Array.Sort(Array) method, which implicitly calls the CompareInfo.Compare method, it defines an System.Collections.Generic.IComparer<T> implementation that compares sort keys, which it instantiates and passes to the Array.Sort<T>(T[], IComparer<T>) method.

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

public class SortKeyComparer : IComparer<String>
{
   public int Compare(string str1, string str2)
   {
      SortKey sk1, sk2;
      sk1 = CultureInfo.CurrentCulture.CompareInfo.GetSortKey(str1);         
      sk2 = CultureInfo.CurrentCulture.CompareInfo.GetSortKey(str2); 
      return SortKey.Compare(sk1, sk2);        
   }
}

public class Example
{
   public static void Main()
   {
      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

If at all possible, avoid using composite strings that are built at run time from concatenated phrases. Composite strings are difficult to localize, because they often assume a grammatical order in the app's original language that does not apply to other localized languages.

How you handle date and time values depends on whether they are displayed in the user interface or persisted. This section examines both usages. It also discusses how you can handle time zone differences and arithmetic operations when working with dates and times.

Typically, when dates and times are displayed in the user interface, you should use the formatting conventions of the user's culture, which is defined by the CultureInfo.CurrentCulture property and by the DateTimeFormatInfo object returned by the CultureInfo.CurrentCulture.DateTimeFormat property. The formatting conventions of the current culture are automatically used when you format a date by using any of these methods:

The following example displays sunrise and sunset data twice for October 11, 2012. It first sets the current culture to Croatian (Croatia), and then to English (Great Britain). In each case, the dates and times are displayed in the format that is appropriate for that culture.

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

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

   public static void Main()
   {
      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

You should never persist date and time data in a format that can vary by culture. This is a common programming error that results in either corrupted data or a run-time exception. The following example serializes two dates, January 9, 2013 and August 18, 2013, as strings by using the formatting conventions of the English (United States) culture. When the data is retrieved and parsed by using the conventions of the English (United States) culture, it is successfully restored. However, when it is retrieved and parsed by using the conventions of the English (United Kingdom) culture, the first date is wrongly interpreted as September 1, and the second fails to parse because the Gregorian calendar does not have an eighteenth month.

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

public class Example
{
   public static void Main()
   {
      // 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

You can avoid this problem in any of three ways:

  • Serialize the date and time in binary format rather than as a string.

  • Save and parse the string representation of the date and time by using a custom format string that is the same regardless of the user's culture.

  • Save the string by using the formatting conventions of the invariant culture.

The following example illustrates the last approach. It uses the formatting conventions of the invariant culture returned by the static CultureInfo.InvariantCulture property.

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

public class Example
{
   public static void Main()
   {
      // 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

A date and time value can have multiple interpretations, ranging from a general time ("The stores open on January 2, 2013, at 9:00 A.M.") to a specific moment in time ("Date of birth: January 2, 2013 6:32:00 A.M."). When a time value represents a specific moment in time and you restore it from a serialized value, you should ensure that it represents the same moment in time regardless of the user's geographical location or time zone.

The following example illustrates this problem. It saves a single local date and time value as a string in three standard formats ("G" for general date long time, "s" for sortable date/time, and "o" for round-trip date/time) as well as in binary format.

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public class Example
{
   public static void Main()
   {
      BinaryFormatter formatter = new BinaryFormatter();

      DateTime dateOriginal = new DateTime(2013, 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");
      } 
      // Serialize the data as a binary value. 
      if (! File.Exists("DateInfo.bin")) {
         FileStream fsIn = new FileStream("DateInfo.bin", FileMode.Create);
         formatter.Serialize(fsIn, dateOriginal);
         fsIn.Close();
         Console.WriteLine("Serialized date to DateInfo.bin");
      }
      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);
      }
      Console.WriteLine();

      // Restore the date from binary data.
      FileStream fsOut = new FileStream("DateInfo.bin", FileMode.Open);
      DateTime restoredDate = (DateTime) formatter.Deserialize(fsOut);
      Console.WriteLine("{0} {1}", restoredDate, restoredDate.Kind);
   }
}

When the data is restored on a system in the same time zone as the system on which it was serialized, the deserialized date and time values accurately reflect the original value, as the output shows:

'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

3/30/2013 6:00:00 PM Local

However, if you restore the data on a system in a different time zone, only the date and time value that was formatted with the "o" (round-trip) standard format string preserves time zone information and therefore represents the same instant in time. Here's the output when the date and time data is restored on a system in the Romance Standard Time zone:

'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/31/2013 3:00:00 AM Local

3/30/2013 6:00:00 PM Local

To accurately reflect a date and time value that represents a single moment of time regardless of the time zone of the system on which the data is deserialized, you can do any of the following:

  • Save the value as a string by using the "o" (round-trip) standard format string. Then deserialize it on the target system.

  • Convert it to UTC and save it as a string by using the "r" (RFC1123) standard format string. Then deserialize it on the target system and convert it to local time.

  • Convert it to UTC and save it as a string by using the "u" (universal sortable) standard format string. Then deserialize it on the target system and convert it to local time.

  • Convert it to UTC and save it in binary format. Then deserialize it on the target system and convert it to local time.

The following example illustrates each technique.

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public class Example
{
   public static void Main()
   {
      BinaryFormatter formatter = new BinaryFormatter();

      // Serialize a date.
      DateTime dateOriginal = new DateTime(2013, 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();
         Console.WriteLine("Serialized dates to DateInfo.dat");
      }   
      // Serialize the date as a binary value. 
      if (! File.Exists("DateInfo2.bin")) {
         FileStream fsIn = new FileStream("DateInfo2.bin", FileMode.Create);
         formatter.Serialize(fsIn, dateOriginal.ToUniversalTime());
         fsIn.Close();
         Console.WriteLine("Serialized date to DateInfo.bin");
      }
      Console.WriteLine();

      // 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("'{0}' --> {1} {2}", 
                              dateStrings[ctr], newDate, newDate.Kind);
         }
         else {
            DateTime newLocalDate = newDate.ToLocalTime();
            Console.WriteLine("'{0}' --> {1} {2}", 
                              dateStrings[ctr], newLocalDate, newLocalDate.Kind);
         }
      }
      Console.WriteLine();

      // Restore the date from binary data.
      FileStream fsOut = new FileStream("DateInfo2.bin", FileMode.Open);
      DateTime restoredDate = (DateTime) formatter.Deserialize(fsOut);
      restoredDate = restoredDate.ToLocalTime();
      Console.WriteLine("{0} {1}", restoredDate, restoredDate.Kind);
   }
}

When the data is serialized on a system in the Pacific Standard Time zone and deserialized on a system in the Romance Standard Time zone, the example displays the following output:

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

3/31/2013 3:00:00 AM Local

For more information, see Converting Times Between Time Zones.

Both the DateTime and DateTimeOffset types support arithmetic operations. You can calculate the difference between two date values, or you can add or subtract particular time intervals to or from a date value. However, arithmetic operations on date and time values do not take time zones and time zone adjustment rules into account. Because of this, date and time arithmetic on values that represent moments in time can return inaccurate results.

For example, the transition from Pacific Standard Time to Pacific Daylight Time occurs on the second Sunday of March, which is March 10 for the year 2013. As the following example shows, if you calculate the date and time that is 48 hours after March 9, 2013 at 10:30 A.M. on a system in the Pacific Standard Time zone, the result, March 11, 2013 at 10:30 A.M., does not take the intervening time adjustment into account.

using System;

public class Example
{
   public static void Main()
   {
      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

To ensure that an arithmetic operation on date and time values produces accurate results, follow these steps:

  1. Convert the time in the source time zone to UTC.

  2. Perform the arithmetic operation.

  3. If the result is a date and time value, convert it from UTC to the time in the source time zone.

The following example is similar to the previous example, except that it follows these three steps to correctly add 48 hours to March 9, 2013 at 10:30 A.M.

using System;

public class Example
{
   public static void Main()
   {
      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

For more information, see Performing Arithmetic Operations with Dates and Times.

Your app may need to display the name of the month or the day of the week. To do this, code such as the following is common.

using System;

public class Example
{
   public static void Main()
   {
      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.

However, this code always returns the names of the days of the week in English. Code that extracts the name of the month is often even more inflexible. It frequently assumes a twelve-month calendar with names of months in a specific language.

By using custom date and time format strings or the properties of the DateTimeFormatInfo object, it is easy to extract strings that reflect the names of days of the week or months in the user's culture, as the following example illustrates. It changes the current culture to French (France) and displays the name of the day of the week and the name of the month for July 1, 2013.

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

public class Example
{
   public static void Main()
   {
      // Set the current thread culture to French (France).
      Thread.CurrentThread.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.

The handling of numbers depends on whether they are displayed in the user interface or persisted. This section examines both usages.

Note Note

In parsing and formatting operations, the .NET Framework recognizes only the Basic Latin characters 0 through 9 (U+0030 through U+0039) as numeric digits.

Typically, when numbers are displayed in the user interface, you should use the formatting conventions of the user's culture, which is defined by the CultureInfo.CurrentCulture property and by the NumberFormatInfo object returned by the CultureInfo.CurrentCulture.NumberFormat property. The formatting conventions of the current culture are automatically used when you format a date by using any of the following methods:

  • The parameterless ToString method of any numeric type

  • The ToString(String) method of any numeric type, which includes a format string as an argument

  • The composite formatting feature, when it is used with numeric values

The following example displays the average temperature per month in Paris, France. It first sets the current culture to French (France) before displaying the data, and then sets it to English (United States). In each case, the month names and temperatures are displayed in the format that is appropriate for that culture. Note that the two cultures use different decimal separators in the temperature value. Also note that the example uses the "MMMM" custom date and time format string to display the full month name, and that it allocates the appropriate amount of space for the month name in the result string by determining the length of the longest month name in the DateTimeFormatInfo.MonthNames array.

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

public class Example
{
   public static void Main()
   {
      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

You should never persist numeric data in a culture-specific format. This is a common programming error that results in either corrupted data or a run-time exception. The following example generates ten random floating-point numbers, and then serializes them as strings by using the formatting conventions of the English (United States) culture. When the data is retrieved and parsed by using the conventions of the English (United States) culture, it is successfully restored. However, when it is retrieved and parsed by using the conventions of the French (France) culture, none of the numbers can be parsed because the cultures use different decimal separators.

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

public class Example
{
   public static void Main()
   {
      // 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'

To avoid this problem, you can use one of these techniques:

  • Save and parse the string representation of the number by using a custom format string that is the same regardless of the user's culture.

  • Save the number as a string by using the formatting conventions of the invariant culture, which is returned by the CultureInfo.InvariantCulture property.

  • Serialize the number in binary instead of string format.

The following example illustrates the last approach. It serializes the array of Double values, and then deserializes and displays them by using the formatting conventions of the English (United States) and French (France) cultures.

using System;
using System.Globalization;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;

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

      // Serialize the array.
      FileStream fsIn = new FileStream("randoms.dat", FileMode.Create);
      BinaryFormatter formatter = new BinaryFormatter();
      formatter.Serialize(fsIn, numbers);
      fsIn.Close();

      // Read the persisted data.
      FileStream fsOut = new FileStream("randoms.dat", FileMode.Open);
      double[] numbers1 = (Double[]) formatter.Deserialize(fsOut);      
      fsOut.Close();

      // Display the data using the conventions of the en-US culture.
      Console.WriteLine("Current Culture: {0}", 
                        Thread.CurrentThread.CurrentCulture.DisplayName); 
      DisplayRandomNumbers(numbers1);

      // 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); 
      DisplayRandomNumbers(numbers1);
   }

   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: 
//       932.10070623648392 
//       96.868112262742642 
//       857.111520067375 
//       771.37727233179726 
//       262.65733840999064 
//       387.00796914613244 
//       557.49389788019187 
//       83.79498919648816 
//       957.31006048494487 
//       996.54487892824454 
//        
//       Current Culture: English (United States) 
//       932.10070623648392 
//       96.868112262742642 
//       857.111520067375 
//       771.37727233179726 
//       262.65733840999064 
//       387.00796914613244 
//       557.49389788019187 
//       83.79498919648816 
//       957.31006048494487 
//       996.54487892824454 
//        
//       Current Culture: French (France) 
//       932,10070623648392 
//       96,868112262742642 
//       857,111520067375 
//       771,37727233179726 
//       262,65733840999064 
//       387,00796914613244 
//       557,49389788019187 
//       83,79498919648816 
//       957,31006048494487 
//       996,54487892824454

Serializing currency values is a special case. Because a currency value depends on the unit of currency in which it is expressed; it makes little sense to treat it as an independent numeric value. However, if you save a currency value as a formatted string that includes a currency symbol, it cannot be deserialized on a system whose default culture uses a different currency symbol, as the following example shows.

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

public class Example
{
   public static void Main()
   {
      // 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'

Instead, you should serialize the numeric value along with some cultural information, such as the name of the culture, so that the value and its currency symbol can be deserialized independently of the current culture. The following example does that by defining a CurrencyValue structure with two members: the Decimal value and the name of the culture to which the value belongs.

using System;
using System.Globalization;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;

public class Example
{
   public static void Main()
   {
      // 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); 

      // Serialize the currency data.
      BinaryFormatter bf = new BinaryFormatter();
      FileStream fw = new FileStream("currency.dat", FileMode.Create);
      CurrencyValue data = new CurrencyValue(value, CultureInfo.CurrentCulture.Name);
      bf.Serialize(fw, data);
      fw.Close();
      Console.WriteLine();

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

      // Deserialize the data.
      FileStream fr = new FileStream("currency.dat", FileMode.Open);
      CurrencyValue restoredData = (CurrencyValue) bf.Deserialize(fr);
      fr.Close();

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

[Serializable] internal struct CurrencyValue
{
   public CurrencyValue(Decimal amount, string name)
   {
      this.Amount = amount; 
      this.CultureName = name;
   }

   public Decimal Amount;
   public string CultureName;      
}
// 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

In the .NET Framework, the CultureInfo class represents a particular culture or region. Some of its properties return objects that provide specific information about some aspect of a culture:

In general, do not make any assumptions about the values of specific CultureInfo properties and their related objects. Instead, you should view culture-specific data as subject to change, for these reasons:

  • Individual property values are subject to change and revision over time, as data is corrected, better data becomes available, or culture-specific conventions change.

  • Individual property values may vary across versions of the .NET Framework or operating system versions.

  • The .NET Framework supports replacement cultures. This makes it possible to define a new custom culture that either supplements existing standard cultures or completely replaces an existing standard culture.

  • The user can customize culture-specific settings by using the Region and Language app in Control Panel. When you instantiate a CultureInfo object, you can determine whether it reflects these user customizations by calling the CultureInfo.CultureInfo(String, Boolean) constructor. Typically, for end-user apps, you should respect user preferences so that the user is presented with data in a format that he or she expects.

Show:
© 2014 Microsoft