Szymon Kobalczyk's Blog

A Developer's Notebook

  Home  |   Contact  |   Syndication    |   Login
  106 Posts | 6 Stories | 576 Comments | 365 Trackbacks

News

View Szymon Kobalczyk's profile on LinkedIn

Twitter












Tag Cloud


Article Categories

Archives

Post Categories

Blogs I Read

Tools I Use

Last week I've been looking for a better RSS Reader (err... Aggregator). You see after certain number of feeds MS Outlook no longer works for me and I needed a better alternative. In my search I came a cross a product called RikReader and decided to give it a try. Why? I've read some good opinions about it, it's free, it uses Windows RSS Platform, and is close to my heart because it is done entirely in WPF.

I promptly downloaded and installed the package. But instead of the expected welcome screen here is what I get every time I try to run it:

image

(The error message is in Polish but it says: The input string has incorrect format)

As you can see the application includes a very nice error reporting window (every modern application should have one!) so Douglas Stockwell, its author is probably already aware of the problem and hopefully working on the solution. But I think he wouldn't mind to give him a hand.

So what exactly is this problem here? From the stack trace above we can see that it all start in a custom binding converter called MultiplyConverter. My guess is that when this converter expects a decimal number, but encounters a String argument it first tries to convert it to a Double value by calling System.Convert.ToDouble. In the end this method rises exception to indicate that the given text doesn't seem to represent a valid decimal number.

Looks like a pretty obvious bug, so you may wonder why the author didn't fixed it already? You say he can't reproduce it? So what is so special about my system that I always get the same error? The answer is this: it uses different language settings! Should it matter?

Not all of my English-speaking fellow developers might realize this (and those who do sometimes "forget" about it) but not all cultures use the same format to write numbers or dates. You see in certain languages, like French (or in my case in Polish), we write decimal number where the decimal separator is not a dot as in English but a coma. So instead of 123.456 we would write 123,456. A subtle difference but with huge implications (at least for the dumb computer).

Here is a simple example to demonstrate this. The .NET library provides several ways to convert a number to a string:

double number = 123.456;
string convertToString = Convert.ToString(number);
string numberToString = number.ToString();
string stringFormat = String.Format("{0}", number);

What do you think would be the value of each of these strings after running this code? The correct answer is it depends. For some systems this might be  "123.456", but for others this could as well be "123,456". This is because by default, each of these methods uses the current culture settings to produce the text representation of the numerical value.

Similar there are several ways in .NET to parse the text to produce a decimal value:

string text = "123.456"; 
double convertToDouble = Convert.ToDouble(text); 
double doubleParse = Double.Parse(text); 
double doubleTryParse; 
if (Double.TryParse(text, out doubleTryParse) == false) 
    Console.WriteLine("Can't parse string.");

Is this code correct? It also depends in which culture you run it. In some the code executes fine and each double variable gets the correct number. In others the first two methods would throw exceptions and the last one will return false to indicate the string is invalid.

What this all means for you? If your application depends on storing decimal values in certain formats — for example attempts to read some settings from a text file — it can never depend on these methods alone. But not all is lost, and of course .NET provides means to handle such problem.

Each of these methods has overrides that can take IFormatProvider as an argument. How do we find one?. It could be hard to guess at first but in this case you should use appropriate instance of CultureInfo class. So if you expect that the input value you need to parse will be formated in Polish use CultureInfo("pl-pl"). The same applies to output. Of course sometimes it might be hard to determine what culture you should use. But if you are in a lucky position that your application controls both input and output format then you can just pick any culture and use for both. Actually .NET already provides a nice shortcut for this via CultureInfo.InvariantCulture.

I hope that by now you already know how to fix the above examples. Here is the corrected version that uses InvariantCulture:

string convertToString = Convert.ToString(number, CultureInfo.InvariantCulture); 
string numberToString = number.ToString(CultureInfo.InvariantCulture); 
string stringFormat = String.Format(CultureInfo.InvariantCulture, "{0}", number);

double convertToDouble = Convert.ToDouble(text, CultureInfo.InvariantCulture); 
double doubleParse = Double.Parse(text, CultureInfo.InvariantCulture); 
double doubleTryParse; 
if (Double.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleTryParse) == false) 
    Console.WriteLine("Can't parse string.");

As you see this change is fairly simple so I strongly encourage you to review your code and always consider what would be the proper format of the input or output value. The same rules apply to other decimal data types (Float and Decimal), but also to formating dates (DateTime).

If you have a large, existing application reviewing it quickly can be bit cumbersome, but thankfully FxCop contains set of rules that can help find such spots. Here is the code analysis report obtained by running FxCop on the first version of the above examples:

image

As you can see, it properly lists all lines with calls to methods that have overrides that take IFormatProvider (apart from Double.TryParse). With the corrected version of code all these warnings disappear.

Also worth noting is that all methods used in the first example are actually shortcuts for passing the CultureInfo.CurrentCulture value that (if you haven't changed it in your code) holds the culture of your operating system. For most purposes, like displaying the value to the user, this works fine and you could leave it as it is. But when reading files or any external data-source you should always use exactly the same culture that was used to write it.

Hopefully this information would help you avoid very common globalization errors that are quite annoying to many people living in this part of the world. If you need help ensuring that your application runs with no problems on non-English systems just e-mail me and we can test it together.

You can download the sample project here

posted on Sunday, September 16, 2007 8:50 PM

Feedback

# re: Avoid common globalization errors in .NET 9/20/2007 11:49 AM Benj
Thanks for this - I've spent the last few week globalising an airline website in ASP.NET 2 and the abundance of calls to the parse method of float and decimal types has been causing exactly this problem when the culture changes from English. You've provided an excellent quick fix!

# re: Avoid common globalization errors in .NET 9/21/2007 9:15 AM Szymon
Ben,
I'm glad that my post helped you in your work. I encountered such bugs many times, but I also realize that this is not something obvious to anyone grown up as english-speaker. If you have any other experiences to share from globalizing your website please add comments.

# re: Avoid common globalization errors in .NET 10/8/2007 4:13 PM Yury Pahomaŭ
Bout RSS reader - why not google reader? it is fast, has fast search, and most important thing - it's accessible from any computer.

# re: Avoid common globalization errors in .NET 10/10/2007 9:06 AM Szymon
Yury,
To be honest I've never tried Google Reader although many of my friends use it and recommended it to me. But for me it has one fundamental flaw: it is an online reader and I want to read my RSS feeds also when I don't have internet connection (be it on train, on plane or just out of town). Therefore my preferred reader must work the same way as my email client does: prefetch all the news and store on my hard drive. The advantage I see in Google Reader is that I could access my account from many machines. But I found that I can do the same with FeedDemon that I was evaluating for last month (but I still need to check the GreatNews reader before I purchase it).

# re: Avoid common globalization errors in .NET 2/27/2008 7:12 PM Karim
Thanks ,
I am using MySQL in the current project and the DOT or COMA makes a huge diference. This fix help me a lot.

# re: Avoid common globalization errors in .NET 3/31/2008 2:14 PM VJ Koganti
I tried to convert Polisth to English and this doesnt work using invariant culture:

Thread.CurrentThread.CurrentCulture = new CultureInfo("en-GB");
myvar += "Show in English:" + float.Parse("30,2", CultureInfo.InvariantCulture) + "<BR>";

//expected output: 30.2
//actual output: 302

# re: Avoid common globalization errors in .NET 3/31/2008 2:34 PM VJ Koganti
Ok. I found the solution. Using Invariant culture is not going to work. Here is what worked for me.

Thread.CurrentThread.CurrentUICulture = new CultureInfo("pl-PL");
Thread.CurrentThread.CurrentCulture = new CultureInfo("pl-PL");
myvar += "Convert from Polish to Polish:" + float.Parse("30,2", CultureInfo.CurrentCulture) + "<BR>";

Thread.CurrentThread.CurrentUICulture = new CultureInfo("pl-PL");
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-GB");
myvar += "Convert from Polish to English:" + float.Parse("30,2", CultureInfo.CurrentUICulture) + "<BR>";

Thread.CurrentThread.CurrentUICulture = new CultureInfo("pl-PL");
Thread.CurrentThread.CurrentCulture = new CultureInfo("pl-PL");
myvar += "Convert from English to Polish:" + float.Parse("30.2", CultureInfo.InvariantCulture) + "<BR>";

Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-GB");
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-GB");
myvar += "Convert from English to English:" + float.Parse("30.2", CultureInfo.InvariantCulture) + "<BR>";

# re: Avoid common globalization errors in .NET 3/31/2008 4:27 PM VJ Koganti
Made a change in the code. Here is the revised one that works in all cases:

string myvar = string.Empty;

Thread.CurrentThread.CurrentUICulture = new CultureInfo("pl-PL");
Thread.CurrentThread.CurrentCulture = new CultureInfo("pl-PL");
myvar += "Converted from Polish to Polish:" + float.Parse("30,2", CultureInfo.CurrentUICulture) + "<BR>";

Thread.CurrentThread.CurrentUICulture = new CultureInfo("pl-PL");
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-GB");
myvar += "Converted from Polish to English:" + float.Parse("30,2", CultureInfo.CurrentUICulture) + "<BR>";

Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-GB");
Thread.CurrentThread.CurrentCulture = new CultureInfo("pl-PL");
myvar += "Converted from English to Polish:" + float.Parse("30.2", CultureInfo.CurrentUICulture) + "<BR>";

Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-GB");
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-GB");
myvar += "Converted from English to English:" + float.Parse("30.2", CultureInfo.CurrentUICulture) + "<BR>";

lbl.Text = myvar.ToString();

Post A Comment
Title:
Name:
Email:
Comment:
Verification: