Converting Numeric Strings to Double in NET: Culture and Regional Setting Issues

by steventailor in Circuits > Software

4361 Views, 2 Favorites, 0 Comments

Converting Numeric Strings to Double in NET: Culture and Regional Setting Issues

microsoft-net (1).jpg

Commonly, converting numeric string may seem to be not so difficult or complicated task. However, there are some cases, when you can get strange exceptions, that can perplex and confuse you. One of such instances is converting the string representation of the binary value to a numeric value. Ideally, you should store numbers as numbers rather than numbers as strings (especially in the database), but different needs can cause this, e.g., old systems, conversion between some apps, extracting data from the files or even a request from the customer for their domestic needs and so on.

So, where can the problem lay, and what could be a reason for it?

Experienced .NET developers know that there are few ways to convert the string representation of a number into a numeric type:

1. all numeric types have two static parsing methods for this:

  • Parse method throws an exception (FormatException, OverflowException, InvalidCastException) if the operation fails;
  • TryParse returns false if the operation failed, and the resulting value remains 0;

2. through the Convert class, which allows converting a one base data type to another base data type. If the operation fails, it can throw one of the exceptions FormatException, OverflowException, InvalidCastException.

Generally, the string representations of numeric values are interpreted based on the formatting conventions of a particular culture. Such elements as decimal separators, currency marks, groups separators and so on, can be a part of the string and also differ by culture. As a result, the conventions of the string with one culture might fail when using another. Parsing methods always recognize culture-specific variations, so, if you do not specify the format provider; as a result, the format provider associated with the current thread culture (the NumberFormatInfo object returned by the NumberFormatInfo.CurrentInfo property) or, in some cases, the present application domain culture will be used. Using the Convert.ToDouble(String) method is equivalent to passing a value to the Double.Parse(String) method. So this means that the value is interpreted by using the formatting conventions of the current thread culture also (if you do not specify the culture explicitly). In all these cases, the same code will produce different results for different locales.

For example, this line of code can cause a FormatException exception, if you specify a Sweden locale or the current thread culture is equal to it:

String number = “0.84”; 
Double convertedNum = Convert.ToDouble(number); 

Let Us Review This in Practice

image1.png

For this, create a new project by clicking on the Visual Studio menu “File” -> “New” -> ”Project”. In the opened window, select “Console App (.NET Framework),” as it is shown above.

When you have a new project, copy this piece of code below to try the following example, which converts string representations of different Double values with the Convert.ToDouble method and different cultures:

using System; 

using System.Globalization;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            string formatter = "{0,-22}{1,-20}{2}";            string[] values = { "1,304.16", "1 304,16", "1,094", "152.2",
                          "123,45 €", "$1,456.78" };
            double result;
            CultureInfo[] cultureList =
            {
                CultureInfo.CreateSpecificCulture("en-US"),
                CultureInfo.CreateSpecificCulture("fr-FR"),
                CultureInfo.CreateSpecificCulture("sv")
            };
            Console.WriteLine(formatter, "String to convert","Value/Exception", "Culture");
            Console.WriteLine(formatter, "-----------------","-----------------", "------------------");
            foreach (string value in values)
            {
                foreach (CultureInfo cultureInfo in cultureList)
                {
                    try
                    {
                        result = Convert.ToDouble(value, cultureInfo);
                        Console.WriteLine(formatter, value, result, cultureInfo.DisplayName);
                    }
                    catch (FormatException)
                    {
                        Console.WriteLine(formatter, value, "FormatException", cultureInfo.DisplayName);
                    }
                    catch (OverflowException)
                    {
                        Console.WriteLine(formatter, value, "OverflowException", cultureInfo.DisplayName);
                    }
                }
                Console.WriteLine(formatter, "-----------------", "-----------------", "------------------");
            }
            Console.ReadKey();
        }
    }
}

The example shows the following output:

String to convert     Value/Exception     Culture
-----------------     -----------------   ------------------
1,304.16              1304.16             English (United States)
1,304.16              FormatException     French (France)
1,304.16              FormatException     Swedish (Sweden)
-----------------     -----------------   ------------------
1 304,16              FormatException     English (United States)
1 304,16              1304.16             French (France)
1 304,16              1304.16             Swedish (Sweden)
-----------------     -----------------   ------------------
1,094                 1094                English (United States)
1,094                 1.094               French (France)
1,094                 1.094               Swedish (Sweden)
-----------------     -----------------   ------------------
152.2                 152.2               English (United States)
152.2                 FormatException     French (France)
152.2                 FormatException     Swedish (Sweden)
-----------------     -----------------   ------------------
123,45 ?              FormatException     English (United States)
123,45 ?              FormatException     French (France)
123,45 ?              FormatException     Swedish (Sweden)
-----------------     -----------------   ------------------
$1,456.78             FormatException     English (United States)
$1,456.78             FormatException     French (France)
$1,456.78             FormatException     Swedish (Sweden)
-----------------     -----------------   ------------------

So, as you can see, the various locales provide different results and without a proper use (without using try/catch block or TryParse method) can throw the errors and crashes of your application. There are a few possible solutions to avoid this (except not to store numeric values in strings), each of which depends on the method, which you use and on the input values.

The first one is to set the proper culture for methods Parse or Convert.ToDouble. The most appropriate is to use the CultureInfo object InvariantCulture, which is culture-independent. It is associated with the English language but not with any country/region; it is also stable over time and across installed cultures and cannot be customized by users. This makes it the best option to be used for operations, such as formatting and parsing, that require culture-independent results. The code below has the default Sweden culture for testing purposes and correctly converts string representations of values, which threw an exception before:

using System; 
using System.Globalization;
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture("sv");
            string formatter = "{0,-22}{1,-20}{2}";            string[] values = { "1,304.16", "1304,16", "1,094", "152.2" };
            double result;
            Console.WriteLine(formatter, "String to convert", "Value/Exception", "Culture");
            Console.WriteLine(formatter, "-----------------", "-----------------", "------------------");
            foreach (string value in values)
            {
                    try
                    {
                        result = Double.Parse(value, CultureInfo.InvariantCulture);
                        Console.WriteLine(formatter, value, result, CultureInfo.CurrentCulture.DisplayName);
                        result = Convert.ToDouble(value, CultureInfo.InvariantCulture);                    
                        Console.WriteLine(formatter, value, result, CultureInfo.CurrentCulture.DisplayName);
                    }
                    catch (FormatException)
                    {
                        Console.WriteLine(formatter, value, "FormatException", CultureInfo.CurrentCulture.DisplayName);
                    }
                    catch (OverflowException)
                    {
                        Console.WriteLine(formatter, value, "OverflowException", CultureInfo.CurrentCulture.DisplayName);
                    }
                Console.WriteLine(formatter, "-----------------", "-----------------", "------------------");
            }
            Console.ReadKey();
        }
    }
}

The example shows the following output:

String to convert     Value/Exception     Culture
-----------------     -----------------   ------------------
1,304.16              1304,16             Swedish (Sweden)
1,304.16              1304,16             Swedish (Sweden)
-----------------     -----------------   ------------------
1304,16               130416              Swedish (Sweden)
1304,16               130416              Swedish (Sweden)
-----------------     -----------------   ------------------
1,094                 1094                Swedish (Sweden)
1,094                 1094                Swedish (Sweden)
-----------------     -----------------   ------------------
152.2                 152,2               Swedish (Sweden)
152.2                 152,2               Swedish (Sweden)
-----------------     -----------------   ------------------

As you can see, in the first example, an exception occurred during the converting of these values. But now all work correctly and coherently as it should. Probably, it is the most common problem with such conversion and locales, and the solution is pretty simple. Nonetheless, this solution will not be useful if you need to convert the currency from the string to double (for example) value. In this case, you can use the TryParse method by setting NumberStyles properties, as it is shown below:

using System; 
using System.Globalization;
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture("sv");
            string formatter = "{0,-22}{1,-20}{2}";            string[] values = { "$123456.78" };
            double result;            Console.WriteLine(formatter, "String to convert", "Value/Exception", "Culture");
            Console.WriteLine(formatter, "-----------------", "-----------------", "------------------");
            foreach (string value in values)
            {
                    try
                    {
                        Double.TryParse(value, NumberStyles.AllowCurrencySymbol | NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands, new CultureInfo("en-US"), out result);
                        Console.WriteLine(formatter, value, result, CultureInfo.CurrentCulture.DisplayName);
                    }
                    catch (FormatException)
                    {
                        Console.WriteLine(formatter, value, "FormatException", CultureInfo.CurrentCulture.DisplayName);
                    }
                    catch (OverflowException)
                    {
                        Console.WriteLine(formatter, value, "OverflowException", CultureInfo.CurrentCulture.DisplayName);
                    }
                Console.WriteLine(formatter, "-----------------", "-----------------", "------------------");
            }
            Console.ReadKey();
        }
    }
} 

The example shows the following output:

String to convert     Value/Exception     Culture
-----------------     -----------------   ------------------
$123456.78            123456,78           Swedish (Sweden)
-----------------     -----------------   ------------------ 

NumberStyles Enum

NumberStyles Enum can be used, when you need to determine the styles permitted in the numeric string. You can set a single style or a bitwise combination of its member values. Possible values determine:

  • AllowCurrencySymbol - that the string can contain a currency symbol;
  • AllowDecimalPoint - that the string can have a decimal point;
  • AllowExponent - that the string can be in exponential notation AllowHexSpecifier - that the string represents a hexadecimal value;
  • AllowLeadingSign - that the string can have a leading sign (positive or negative);
  • AllowLeadingWhite - that the string can contain leading white-space characters. Valid white-space characters have the Unicode values U+0009, U+000A, U+000B, U+000C, U+000D, and U+0020;
  • AllowParentheses - that the string can have one pair of parentheses enclosing the number. The parentheses indicate that the string represents a negative number;
  • AllowThousands - that the string can have group separators, such as symbols that separate hundreds from thousands;
  • AllowTrailingSign - that the string can have a trailing sign;
  • AllowTrailingWhite - that the string can have a trailing white-space characters can be present in the parsed string. Valid white-space characters have the Unicode values U+0009, U+000A, U+000B, U+000C, U+000D, and U+0020.Any - that all styles except AllowHexSpecifier are used; this is a composite number style;
  • Currency - that all styles except AllowExponent and AllowHexSpecifier are used; this is a composite number style;
  • Float - that the AllowLeadingWhite, AllowTrailingWhite, AllowLeadingSign, AllowDecimalPoint, and AllowExponent styles are used; this is a composite number style;
  • HexNumber - that the AllowLeadingWhite, AllowTrailingWhite, and AllowHexSpecifier styles are used; this is a composite number style;
  • Integer - that the AllowLeadingWhite, AllowTrailingWhite, and AllowLeadingSign styles are used; this is a composite number style;
  • None - that the string does not have elements, such as leading or trailing whitespace, thousands of separators, or a decimal separator, only integral decimal digits;
  • Number - that the AllowLeadingWhite, AllowTrailingWhite, AllowLeadingSign, AllowTrailingSign, AllowDecimalPoint, and AllowThousands styles are used; this is a composite number style.

With the NumberStyles enumeration, you can manipulate the string conversion according to your needs; the only one restriction is that you should exactly know which characters can be presented in the string if you set some specific field of the enum. For example, if you set AllowDecimalPoint, but the string contains a leading sign - you will get an error.

The information above describes solutions for common problems with conversion. Some real scenarios can get much more complex and will need additional research, but for now, this article should be good enough for understanding some niceties of conversion and might help beginners.