Standard formatting in .NET is sometimes not enough to fill specific requirements. For example, when high-level financial reports from big corporations need to be produced, the numbers dealt-with are generally around hundred of millions. Consider the following report where the scale and the currency (M$) is being displayed in the row header.

Quarter 1Quarter 2
New York (in M$)102.7621.40
Florida (in M$)32.7667.40

In order to achieve this formatting, a straightforward solution would be to apply a scaling factor to the numbers beforehand.

string.Format("{0:N2}", valueToFormat / 1000000); 

Sometimes, it is not appropriate nor possible to scale the numbers. In this situation, number scaling using the “,” custom specifier could be used. Note the use of two commas since the number to be formatted is divided by 1000 for each comma:

[csharp] decimal valueToFormat = 1345278000.346M; string.Format(“0:#,#,,”, valueToFormat ); // displays 1,345 with the invariant culture [/csharp]

Most of the examples found in the MSDN fail to explain how to scale the formatted number and keep the decimals at the same time. The trick is to add the “.” custom specifier after the “,” comma specifiers:

decimal valueToFormat = 1345278000.346M;
string.Format("0:#,#,,.00", valueToFormat ); // displays 1,345.28 with the invariant culture

This custom formatting technique, however, has some drawbacks:

  • It is harder to read than standard formatting such as {0:N} or {0:C}.
  • The output may not be consistent with the settings of some cultures. For example, forcing to display two decimal digits may not be compatible with the value of CultureInfo.NumberFormat.NumberDecimalDigits.
  • Finally, custom specifiers do not mix with the >standard numeric specifiers. The currency symbol cannot be therefore displayed side-by-side with the number.

A custom formatter called NumberScalingFormatter has been hence developed to allow scaling the numbers while keeping the underlying settings of the culture. In order to use it, pass the ScalingFactor and the underlying culture. The following output illustrates the behavior of the custom formatter as well as the custom “,” specifier combined with the “.” specifier:

// string.Format(CultureInfo.InvariantCulture,{0:N}, 1345278000.346): 
	1,345,278,000.35
// string.Format(CultureInfo.InvariantCulture,{0:#,#,,}, 1345278000.346): 
	1,345
// string.Format(CultureInfo.InvariantCulture,{0:#,#,,.00}, 1345278000.346): 
	1,345.28
// string.Format(new CultureInfo("en-US"),{0:#,#,,.00}, 1345278000.346): 
	1,345.28
// string.Format(new CultureInfo("fr-FR"),{0:#,#,,.00}, 1345278000.346): 
	1 345,28
// string.Format(CultureInfo.InvariantCulture,{0:N}, 1345278000.346): 
	1,345,278,000.35
// string.Format(new NumberScalingFormatter(ScalingFactor.Million, CultureInfo.InvariantCulture),{0:N}, 1345278000.346): 
	1,345.28
// string.Format(new NumberScalingFormatter(ScalingFactor.Million, new CultureInfo("en-US")),{0:N}, 1345278000.346): 
	1,345.28
// string.Format(new NumberScalingFormatter(ScalingFactor.Million, new CultureInfo("fr-FR")),{0:N}, 1345278000.346): 
	1 345,28

For an introduction to custom formatter, please check out the post from David Hayden. A basic outline of the code follows.

public enum ScalingFactor
{
    None,
    Million,
    Billion
}

public class NumberScalingFormatter : IFormatProvider, ICustomFormatter
{
    private readonly CultureInfo _underlyingCulture;
    private readonly ScalingFactor _scalingFactor;


    public NumberScalingFormatter(ScalingFactor scalingFactor, CultureInfo underlyingCulture)
    {
        if (underlyingCulture == null)
        {
            throw new ArgumentNullException();
        }
        _scalingFactor = scalingFactor;
        _underlyingCulture = underlyingCulture;
    }

    public ScalingFactor Factor
    {
        get
        {
            return _scalingFactor;
        }
    }

    public CultureInfo Culture
    {
        get
        {
            return _underlyingCulture;
        }
    }


    #region IFormatProvider Members

    public object GetFormat(Type formatType)
    {
        object formatter = null;
        if (formatType == typeof(ICustomFormatter))
        {
            formatter = this;
        }
        else
        {
            formatter = _underlyingCulture.GetFormat(formatType);
        }
        return formatter;
    }

    #endregion

    #region ICustomFormatter Members
    private int GetUnderlyingThousandScalingFactor()
    {
        int underlyingThousandScalingFactor;

        switch (_scalingFactor)
        {
            case ScalingFactor.None:
                underlyingThousandScalingFactor = 0;
                break;

            case ScalingFactor.Billion:
                underlyingThousandScalingFactor = 3;
                break;

            default:
            case ScalingFactor.Million:
                underlyingThousandScalingFactor = 2;
                break;
        }
        return underlyingThousandScalingFactor;
    }

    private object Scale(object arg)
    {
        object scaledValue = null;

        if (arg == null)
        {
            scaledValue = null;
        }
        else if (_scalingFactor == ScalingFactor.None)
        {
            scaledValue = arg;
        }
        else
        {
            int underlyingThousandScalingFactor = GetUnderlyingThousandScalingFactor();
            try
            {
                double convertedValue = Convert.ToDouble(arg);
                scaledValue = Math.Pow(10, underlyingThousandScalingFactor * -3) * convertedValue;
            }
            catch (InvalidCastException)
            {

            }
        }
        return scaledValue;
    }

    public string Format(string format, object arg, IFormatProvider formatProvider)
    {
        StringBuilder formattableString = new StringBuilder("{0");
        if (!string.IsNullOrEmpty(format))
        {
            formattableString.Append(":");
            formattableString.Append(format);
        }
        formattableString.Append("}");

        return string.Format(_underlyingCulture, formattableString.ToString(), Scale(arg));
    }

    #endregion
}

Download icon The source code for this post is available from github.