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.

Use Unicode Internally

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.

Use Resource Files

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 information about creating and deploying satellite assemblies, see Creating Satellite Assemblies and Packaging and Deploying Resources.

Searching and Comparing Strings

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.

System_CAPS_ICON_tip.jpg 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.

Imports System.Globalization
Imports System.Threading

Module Example
   Public Sub Main()
      Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("pl-PL")
      Dim composite As String = ChrW(&h0041) + ChrW(&H0300) 
      Console.WriteLine("Comparing using Char:   {0}", composite.IndexOf(ChrW(&h00C0)))
      Console.WriteLine("Comparing using String: {0}", composite.IndexOf(ChrW(&h00C0).ToString()))
   End Sub 
End Module
' 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.

Testing Strings for Equality

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.

Imports System.Globalization
Imports System.Threading

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

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

Imports System.Globalization
Imports System.Threading

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

Ordering and Sorting Strings

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.

Imports System.Globalization
Imports System.Threading

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

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. Note that this list of supported Unicode versions applies to character comparison and sorting only; it does not apply to classification of Unicode characters by category. For more information, see the "Strings and The Unicode Standard" section in the String article.

.NET Framework versionOperating systemUnicode version
.NET Framework 2.0All operating systemsUnicode 4.1
.NET Framework 3.0All operating systemsUnicode 4.1
.NET Framework 3.5All operating systemsUnicode 4.1
.NET Framework 4All operating systemsUnicode 5.0
.NET Framework 4.5Windows 7Unicode 5.0
.NET Framework 4.5Windows 8Unicode 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.

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

Avoid String Concatenation

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.

Displaying 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.

Imports System.Globalization
Imports System.Threading

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

   Public Sub Main()
      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

Persisting Dates and Times

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.

Imports System.Globalization
Imports System.IO
Imports System.Threading

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

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.

Imports System.Globalization
Imports System.IO
Imports System.Threading

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

Serialization and Time Zone Awareness

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.

Imports System.IO
Imports System.Runtime.Serialization.Formatters.Binary

Module Example
   Public Sub Main()
      Dim formatter As New BinaryFormatter()
      
      ' Serialize a date.
      Dim dateOriginal As Date = #03/30/2013 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()
         Console.WriteLine("Serialized dates to DateInfo.dat")
      End If   
      ' Serialize the date as a binary value.
      If Not File.Exists("DateInfo.bin") Then
         Dim fsIn As New FileStream("DateInfo.bin", FileMode.Create)
         formatter.Serialize(fsIn, dateOriginal)
         fsIn.Close()
         Console.WriteLine("Serialized date to DateInfo.bin")
      End If
      Console.WriteLine()
      
      ' 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
      Console.WriteLine()
      
      ' Restore the date from binary data.
      Dim fsOut As New FileStream("DateInfo.bin", FileMode.Open)
      Dim restoredDate As Date = DirectCast(formatter.Deserialize(fsOut), DateTime)
      Console.WriteLine("{0} {1}", restoredDate, restoredDate.Kind)
   End Sub
End Module

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.

Imports System.IO
Imports System.Runtime.Serialization.Formatters.Binary

Module Example
   Public Sub Main()
      Dim formatter As New BinaryFormatter()
      
      ' Serialize a date.
      Dim dateOriginal As Date = #03/30/2013 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()
         Console.WriteLine("Serialized dates to DateInfo.dat")
      End If   
      ' Serialize the date as a binary value.
      If Not File.Exists("DateInfo2.bin") Then
         Dim fsIn As New FileStream("DateInfo2.bin", FileMode.Create)
         formatter.Serialize(fsIn, dateOriginal.ToUniversalTime())
         fsIn.Close()
         Console.WriteLine("Serialized date to DateInfo.bin")
      End If
      Console.WriteLine()
      
      ' 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
      Console.WriteLine()
      
      ' Restore the date from binary data.
      Dim fsOut As New FileStream("DateInfo2.bin", FileMode.Open)
      Dim restoredDate As Date = DirectCast(formatter.Deserialize(fsOut), DateTime)
      restoredDate = restoredDate.ToLocalTime()
      Console.WriteLine("{0} {1}", restoredDate, restoredDate.Kind)
   End Sub
End Module

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.

Performing Date and Time Arithmetic

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.

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

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.

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

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

Using Culture-Sensitive Names for Date Elements

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.

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

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.

Imports System.Globalization
Imports System.Threading

Module Example
   Public Sub Main()
      ' Set the current thread culture to French (France).
      Thread.CurrentThread.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.

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

System_CAPS_ICON_note.jpg 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.

Displaying Numeric Values

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.

Imports System.Globalization
Imports System.Threading

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

Persisting Numeric Values

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.

Imports System.Globalization
Imports System.IO
Imports System.Threading

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

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.

Imports System.Globalization
Imports System.IO
Imports System.Runtime.Serialization.Formatters.Binary
Imports System.Threading

Module Example
   Public Sub Main()
      ' Create ten random doubles.
      Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
      Dim numbers() As Double = GetRandomNumbers(10)
      DisplayRandomNumbers(numbers)
      
      ' Serialize the array.
      Dim fsIn As New FileStream("randoms.dat", FileMode.Create)
      Dim formatter As New BinaryFormatter()
      formatter.Serialize(fsIn, numbers)
      fsIn.Close()
      
      ' Read the persisted data.
      Dim fsOut AS New FileStream("randoms.dat", FileMode.Open)
      Dim numbers1() As Double = DirectCast(formatter.Deserialize(fsOut), Double())      
      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)
   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:
'       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.

Imports System.Globalization
Imports System.IO
Imports System.Threading

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

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.

Imports System.Globalization
Imports System.IO
Imports System.Runtime.Serialization.Formatters.Binary
Imports System.Threading

<Serializable> Friend Structure CurrencyValue
   Public Sub New(amount As Decimal, name As String)
      Me.Amount = amount 
      Me.CultureName = name
   End Sub
   
   Public Amount As Decimal
   Public CultureName As String      
End Structure

Module Example
   Public Sub Main()
      ' 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 bf As New BinaryFormatter()
      Dim fw As New FileStream("currency.dat", FileMode.Create)
      Dim data As 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.
      Dim fr AS New FileStream("currency.dat", FileMode.Open)
      Dim restoredData As CurrencyValue = CType(bf.Deserialize(fr), CurrencyValue)
      fr.Close()
      
      ' Display the original 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

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.

Globalization and Localization
Best Practices for Using Strings

Show: