6

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.

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • 1
    Why don't you like value.ToString("n")? – banging Jul 10 '12 at 14:33
  • @banging It only shows 3 decimal places. i also like **General** (`value.ToString("G")`), except that it doesn't show digit grouping. i like the decimals of **General (g)** and the grouping of **Number (n)**. Together they would be unstoppable. – Ian Boyd Jul 10 '12 at 14:52

3 Answers3

7

The ToString method on decimals by default uses the CultureInfo.CurrentCulture for the user's session, and thus varies based on whom is running the code.

The ToString method also accepts an IFormatProvider in various overloads. This is where you need to supply your culture-specific Formatters.

For instance, if you pass the NumberFormat for fr-CH, you can format things as that culture expects:

var culture = CultureInfo.CreateSpecificCulture("fr-CH");
Decimal value = -1234567890.1234789012M;

Console.WriteLine(value.ToString("##,#.###############", culture.NumberFormat));

Will output

 -1'234'567'890.1234789012

Edit #3 - rewrote using custom formatters. This should do what you want based on the new updated question.

Edit #4 - Took all of your input, and ran this:

public void TestOutput()
{
    PrintValue(-1234567890M);
    PrintValue(-1234567890.1M);
    PrintValue(-1234567890.12M);
    PrintValue(-1234567890.123M);
    PrintValue(-1234567890.1234M);
    PrintValue(-1234567890.12347M);
    PrintValue(-1234567890.123478M);
    PrintValue(-1234567890.1234789M);
    PrintValue(-1234567890.12347890M);
    PrintValue(-1234567890.123478901M);
    PrintValue(-1234567890.1234789012M);
}

private static void PrintValue(decimal value)
{
   var culture = CultureInfo.CreateSpecificCulture("qps-PLOC");
   Console.WriteLine(value.ToString("##,#.###############", culture.NumberFormat));
}

Gives output matching what you supplied:

--12,,3456,,7890
--12,,3456,,7890..1
--12,,3456,,7890..12
--12,,3456,,7890..123
--12,,3456,,7890..1234
--12,,3456,,7890..12347
--12,,3456,,7890..123478
--12,,3456,,7890..1234789
--12,,3456,,7890..1234789
--12,,3456,,7890..123478901
--12,,3456,,7890..1234789012

As pointed out by Joshua, this only works for some locales.

From the looks of it then, you need to pick the lesser of two evils: Knowing the precision of your numbers, or specifying formats for each culture. I'd wager knowing the precision of your numbers may be easier. In which case, a previous version of my answer may be of use:

To explicitly control the number of decimal places to output, you can clone the number format provided by the culture and modify the NumberDecimalDigits property.

var culture = CultureInfo.CreateSpecificCulture("fr-CH");
Decimal value = -1234567890.1234789012M;
NumberFormatInfo format = (NumberFormatInfo)culture.NumberFormat.Clone();
format.NumberDecimalDigits = 30;

Console.WriteLine(value.ToString("n", format));

This outputs:

-1'234'567'890.123478901200000000000000000000
  • That truncates the number of decimals to `CultureInfo.NumberFormat.NumberDecimalDigits`. – Ian Boyd Jul 10 '12 at 14:53
  • 1
    Unfortunately using a custom formatting will break negative numbers for some cultures. In your example, *lo-LA* will print `-1234567,890.1234789012` when it should be `(1234567,890.1234789012)`. In addition, the negative symbol is on the left for *ar-DZ*, *prs-AF*, and *ps-AF* instead of on the right. (Such is the bane of custom formatting) – Joshua Jul 10 '12 at 15:18
  • @Joshua ...ack, right, so, using the clone of the number formatter and N is probably... easier and more accurate? –  Jul 10 '12 at 15:21
  • @WillHughes, I guess you can call it more accurate. Dealing with culture is difficult, so I tend to try and rely on Microsoft getting it right in the .Net framework. I also use it to try and be consistent, since I'll be calling to the same underlying framework. I know even Microsoft's gotten it wrong before and fixed it in subsequent patches. – Joshua Jul 10 '12 at 15:32
  • It might require another question, *"How to know the number of decimal places in a Decimal number?"*. It's legitimately a completely different question, that is a duplicate of this one. – Ian Boyd Jul 10 '12 at 16:57
  • @IanBoyd, I linked to that StackOverflow question in my answer, it shows two ways you can extract the number of decimal places in a number. Be sure to give it a look! – Joshua Jul 10 '12 at 20:25
  • Whilst it's not the same code, the [Mono source code dealing with the formatting logic for 'N' format](http://is.gd/FNRgiX) may be useful/educational for how to implement a version of this yourself. It's only about 40 lines. –  Jul 11 '12 at 00:40
5

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.

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
Eric MSFT
  • 3,246
  • 1
  • 18
  • 28
2

You need to include the culture when formatting for your strings. You can either use String.Format and include the culture as the first parameter or use the object's ToString method and use the overload that takes a culture.

The following code produces the expected output (except for gws-FR, it couldn't find a culture with that string).

namespace CultureFormatting {
  using System;
  using System.Globalization;

  class Program {
    public static void Main() {
      Decimal value = -1234567890.1234789012M;
      Print("en-US", value);
      Print("ca-ES", value);
      //print("gws-FR", value);
      Print("fr-CH", value);
      Print("ar-DZ", value);
      Print("prs-AF", value);
      Print("ps-AF", value);
      Print("as-IN", value);
      Print("lo-LA", value);
      Print("qps-PLOC", value);
    }

    static void Print(string cultureName, Decimal value) {
      CultureInfo cultureInfo = new CultureInfo(cultureName);
      cultureInfo.NumberFormat.NumberDecimalDigits = 10;
      // Or, you could replace the {1:N} with {1:N10} to do the same
      // for just this string format call.
      string result = 
        String.Format(cultureInfo, "{0,-8} {1:N}", cultureName, value);
      Console.WriteLine(result);
    }
  }
}

The above code produces the following output:

en-US    -1,234,567,890.1234789012
ca-ES    -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

If you're working with a multithreaded system, such as ASP.Net, you can change the thread's CurrentCulture property. Changing the thread's culture will allow all of the associated ToString and String.Format calls to use that culture.

Update

Since you're wanting to display all of the precision you're going to have to do a bit of work. Using NumberFormat.NumberDecimalDigits will work, except that if the value has less precision, the number will output with trailing zeros. If you need to make sure you display every digit without any extras, you will need to calculate the precision beforehand and set that before you convert it to a string. The StackOverflow question Calculate System.Decimal Precision and Scale may be able to help you determine the precision of the decimal.

Community
  • 1
  • 1
Joshua
  • 8,112
  • 3
  • 35
  • 40