Search code examples
c#.netlocalizationglobalization

Decimal to string with thousand's separators?


Consider a Decimal value:

Decimal value = -1234567890.1234789012M;

i want to convert this Decimal value to a string, and include "thousands separators".

Note: i don't want to include thousand's separators, i want to include digit grouping. The difference is important for cultures that don't group numbers into thousands, or don't use commas to separate groups

Some example output with different standard formatting strings, on my computer, with my current locale:

value.ToString()        =  -1234567890..1234789012   (Implicit General)
value.ToString("g")     =  -1234567890..1234789012   (General)
value.ToString("d")     =          FormatException   (Decimal whole number)
value.ToString("e")     =         -1..234568e++009   (Scientific)
value.ToString("f")     =         -1234567890..123   (Fixed Point)
value.ToString("n")     =     -12,,3456,,7890..123   (Number with commas for thousands)
value.ToString("r")     =          FormatException   (Round trippable)
value.ToString("c")     =   -$$12,,3456,,7890..123   (Currency)
value.ToString("#,0.#") =     -12,,3456,,7890..1

What i want (depending on culture) is:

en-US      -1,234,567,890.1234789012
ca-ES      -1.234.567.890,1234789012
gsw-FR     -1 234 567 890,1234789012    (12/1/2012: fixed gws-FR to gsw-FR)
fr-CH      -1'234'567'890.1234789012
ar-DZ       1,234,567,890.1234789012-
prs-AF      1.234.567.890,1234789012-
ps-AF       1،234،567،890,1234789012-
as-IN     -1,23,45,67,890.1234789012
lo-LA      (1234567,890.1234789012)     (some debate if numbers should be "1,234,567,890")
qps-PLOC  12,,3456,,7890..1234789012

How can i convert a Decimal to a string, with digit groupings?


Update: Some more desired output, using my current culture of :

-1234567890M             -->   -12,,3456,,7890
-1234567890.1M           -->   -12,,3456,,7890..1
-1234567890.12M          -->   -12,,3456,,7890..12
-1234567890.123M         -->   -12,,3456,,7890..123
-1234567890.1234M        -->   -12,,3456,,7890..1234
-1234567890.12347M       -->   -12,,3456,,7890..12347
-1234567890.123478M      -->   -12,,3456,,7890..123478
-1234567890.1234789M     -->   -12,,3456,,7890..1234789
-1234567890.12347890M    -->   -12,,3456,,7890..1234789
-1234567890.123478901M   -->   -12,,3456,,7890..123478901
-1234567890.1234789012M  -->   -12,,3456,,7890..1234789012

Update: i tried peeking at how Decimal.ToString() manages to use the General format to show all the digits that it needs to show:

public override string ToString()
{
    return Number.FormatDecimal(this, null, NumberFormatInfo.CurrentInfo);
}

except that Number.FormatDecimal is hidden somewhere:

[MethodImpl(MethodImplOptions.InternalCall)]
public static extern string FormatDecimal(decimal value, string format, NumberFormatInfo info);

So that's a dead end.


Solution

  • You can specify a custom pattern (the pattern will appropriately resolve to the culture specific method of grouping and the appropriate grouping and decimal separator characters). A pattern can have positive, negative and zero sections. The positive pattern is always the same but the negative pattern depends on the culture and can be retrieved from the NumberFormatInfo's NumberNegativePattern property. Since you want as much precision as possible, you need to fill out 28 digit placeholders after the decimal; the comma forces grouping.

        public static class DecimalFormatters
        {
            public static string ToStringNoTruncation(this Decimal n, IFormatProvider format)
            {
                NumberFormatInfo nfi = NumberFormatInfo.GetInstance(format);
                string[] numberNegativePatterns = {
                        "(#,0.############################)", //0:  (n)
                        "-#,0.############################",  //1:  -n
                        "- #,0.############################", //2:  - n
                        "#,0.############################-",  //3:  n-
                        "#,0.############################ -"};//4:  n -
                var pattern = "#,0.############################;" + numberNegativePatterns[nfi.NumberNegativePattern];
                return n.ToString(pattern, format);
            }
    
            public static string ToStringNoTruncation(this Decimal n)
            {
                return n.ToStringNoTruncation(CultureInfo.CurrentCulture);
            }
        }
    

    Sample output

    Locale    Output
    ========  ============================
    en-US     -1,234,567,890.1234789012
    ca-ES     -1.234.567.890,1234789012
    hr-HR     - 1.234.567.890,1234789012
    gsw-FR    -1 234 567 890,1234789012
    fr-CH     -1'234'567'890.1234789012
    ar-DZ     1,234,567,890.1234789012-
    prs-AF    1.234.567.890,1234789012-
    ps-AF     1،234،567،890,1234789012-
    as-IN     -1,23,45,67,890.1234789012
    lo-LA     (1234567,890.1234789012)
    qps-PLOC  -12,,3456,,7890..1234789012
    

    There is currently no locale that uses NegativeNumberFormat 4 (n -), so that case cannot be tested. But there's no reason to think it would fail.