Number Formatting in .NET [Kit George]

We get an ongoing number of questions about formatting numbers in .NET. The documentation seems to be notoriously difficult to find. I thought I'd give a quick overview here, with a sample.

First note for future reference: to find the write page in the documentation, type in "custom numeric format strings" or "standard numeric format strings" when using the index in the documentation viewer. If you're interested in datetime formatting, try "custom date and time format strings" or "standard date and time format strings".

Formatting is automatically culture-sensitive. So writing out numbers will take the CurrentCulture into account when it formats the values. In the sample below, I use the IFormattable interface to simply force the issue. But changing the System.Threading.Thread.CurrentThread.CurrentCulture would be just as effective.

The next thing to note is that there are a variety of existing format specifiers which define, and support specific formatting. For example, "C" is used to format a number as a currency (which as you can guess, varies wildly with culture). All you need to do is pass the ToString method a formatting specifier (followed by an optional numeric value, indicating the number of decimal places generally). Supported specifiers include fixed point format (F), Number format (N), Exponential format (E) and percentage format (P).

If you're wondering where these values determine how they get formatted, they basically use the NumberFormatInfo associated with the CultureInfo you're using to perform the formatting. If you do nothing else, then it's the CurrentCulture for the currentthread. The NumberFormatInfo has a variety of properties which are used to format numbers for that culture (such as CurrencyDecimalSeparator, and PercentPositivePattern).

You can also specify custom formats if you want, using traditional characters, such as # and 0 (# indicates the formatter should display a numeric value if there is one, or nothing otherwise. 0 indicates that a numeric value should be displayed, or if there is none, then display a zero). For example, "00.00" as a format, will always at least display "00.00". If the number in question is actually 1234, then it will display "1234.00", and if it is actually 0.0234, it will display "00.02". Essentially, digits to the left of the decimal are the minimum number to display (but more significance is always shown), while digits to the right of the decimal are a maximum to display.

A couple of notes on the side:

- A format specifier (used for traditional, well-defined formats) is always a single character long, followed by an optional digit. If you specific a single character that is not recognized, you'll get an exception (For example, "L"). But on the flipside, if you specify more than one character, we'll assume you have a custom format string, so any unrecognized characters in the format are simply written out as literal values. For example, if you try to format an integer using "GG" all you'll see in the output is "GG", since we simply view those as literals

- In the sample below, you'll see "{0, -20}". The ", -20" part formats the output to be the specified width (20). The negative sign says align the text to the left. This gives the output a nice, tabular format

- While I have chosen to use the formats as part of the ToString method in the instance (t.ToString("G")), you could actually stick it into format section specified for the WriteLine call, after the 25, like so: "{1, 25:G}". This can make things simpler

- You'll see question marks in this demo for some cultures, because the Console can't print those characters (such as the currency symbo for the euro)

- for more information on CultureInfo's, see the documentation in the CultureInfo topic. This includes a list of the different cultures you can use

Example: I wrote this using generic collections of course, but to ensure you can take it and run it on V1.1, I've added standard collections back in.

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

class Demo {
// List<CultureInfo> li;
ArrayList li;
public static void Main() {

        Demo d = new Demo();
Decimal d1 = .00000398234m;
Double d2 = 2345987.39876;

d.WriteFormats(d1);

        d.WriteFormats(d2);
}

    public Demo() {
// li = new List<CultureInfo>();
li = new ArrayList();
li.Add(new CultureInfo("en-US"));
li.Add(new CultureInfo("en-GB"));
li.Add(new CultureInfo("fr-FR"));
li.Add(new CultureInfo("es-MX"));
li.Add(new CultureInfo("hi-IN"));
li.Add(new CultureInfo("ja-JP"));
}  

    public void WriteFormats (IFormattable t) {
Console.WriteLine("{0}Value passed was a {1}", Environment.NewLine, t.GetType());

        foreach (CultureInfo ci in li) {
Console.WriteLine("{0}Culture is {1}", Environment.NewLine, ci);
Thread.CurrentThread.CurrentUICulture = ci;

            Console.WriteLine("{0,-20}{1,25}", "General Format:", t.ToString("G", ci));
Console.WriteLine("{0,-20}{1,25}", "Fixed Point Format:", t.ToString("F", ci));
Console.WriteLine("{0,-20}{1,25}", "Exponential Format:", t.ToString("E", ci));
Console.WriteLine("{0,-20}{1,25}", "Percentage Format", t.ToString("P", ci));
Console.WriteLine("{0,-20}{1,25}", "Number Format:", t.ToString("N", ci));
Console.WriteLine("{0,-20}{1,25}", "Number Format (9):", t.ToString("N9", ci));
Console.WriteLine("{0,-20}{1,25}", "Currency Format ():", t.ToString("C", ci));
Console.WriteLine("{0,-20}{1,25}", "Currency Format (9):", t.ToString("C9", ci));
Console.WriteLine("{0,-20}{1,25}", "Custom Format 1:", t.ToString("00.00", ci));
Console.WriteLine("{0,-20}{1,25}", "Custom Format 2:", t.ToString("#,00.##", ci));
}

        Console.WriteLine("{0}Press Enter to continue...", Environment.NewLine);
Console.ReadLine();
}
}

output (for 2345987.39876):

Culture is en-US
General Format: 2345987.39876
Fixed Point Format: 2345987.40
Exponential Format: 2.345987E+006
Percentage Format 234,598,739.88 %
Number Format: 2,345,987.40
Number Format (9): 2,345,987.398760000
Currency Format (): $2,345,987.40
Currency Format (9): $2,345,987.398760000
Custom Format 1: 2345987.40
Custom Format 2: 2,345,987.4

Culture is en-GB
General Format: 2345987.39876
Fixed Point Format: 2345987.40
Exponential Format: 2.345987E+006
Percentage Format 234,598,739.88 %
Number Format: 2,345,987.40
Number Format (9): 2,345,987.398760000
Currency Format (): £2,345,987.40
Currency Format (9): £2,345,987.398760000
Custom Format 1: 2345987.40
Custom Format 2: 2,345,987.4

Culture is fr-FR
General Format: 2345987,39876
Fixed Point Format: 2345987,40
Exponential Format: 2,345987E+006
Percentage Format 234 598 739,88 %
Number Format: 2 345 987,40
Number Format (9): 2 345 987,398760000
Currency Format (): 2 345 987,40 ?
Currency Format (9): 2 345 987,398760000 ?
Custom Format 1: 2345987,40
Custom Format 2: 2 345 987,40

Culture is es-MX
General Format: 2345987.39876
Fixed Point Format: 2345987.40
Exponential Format: 2.345987E+006
Percentage Format 234,598,739.88 %
Number Format: 2,345,987.40
Number Format (9): 2,345,987.398760000
Currency Format (): $2,345,987.40
Currency Format (9): $2,345,987.398760000
Custom Format 1: 2345987.40
Custom Format 2: 2,345,987.4

Culture is hi-IN
General Format: 2345987.39876
Fixed Point Format: 2345987.40
Exponential Format: 2.345987E+006
Percentage Format 23,45,98,739.88 %
Number Format: 23,45,987.40
Number Format (9): 23,45,987.398760000
Currency Format (): ?? 23,45,987.40
Currency Format (9): ?? 23,45,987.398760000
Custom Format 1: 2345987.40
Custom Format 2: 23,45,987.4

Culture is ja-JP
General Format: 2345987.39876
Fixed Point Format: 2345987.40
Exponential Format: 2.345987E+006
Percentage Format 234,598,739.88%
Number Format: 2,345,987.40
Number Format (9): 2,345,987.398760000
Currency Format (): ¥2,345,987
Custom Format 1: 2345987.40
Custom Format 2: 2,345,987.4