Wednesday, May 19, 2010

Custom numeric string formatting, including engineering notation

I needed to display numbers in an engineering application. The customer wanted the numbers formatted nicely, with about four significant digits and engineering notation. .NET has very flexible number formatting. However, it lacks standard formatting into engineering notation.

The extension point for formatting numeric strings is through the IFormatProvider and ICustomFormatter interfaces. I implemented a format provider for the engineering notation. However, exclusively using engineering notation was awkward in our application. As an alternative to exclusively using engineering notation, I decided on the following format, which mixes four significant digits, engineering notation, and thousand separators:

3.1416E-05  31.42E-06
0.00031416  314.2E-06
0.0031416   0.003142
0.031416    0.03142
0.31416     0.3142
3.1416      3.142
31.416      31.42
314.16      314.2
3141.6      3,142
31416       31,416
3.1416E+05  314,159
3.1416E+06  3.142E+06
3.1416E+07  31.42E+06
3.1416E+08  314.2E+06
3.1416E+09  3.142E+09

In case you want similar formatting, here is the class that implements it:

public class EngineeringFormatProvider : IFormatProvider, ICustomFormatter
{
    public object GetFormat(Type formatType)
    {
        return (formatType == typeof(ICustomFormatter)) ? this : null;
    }

    public string Format(string format, object arg, IFormatProvider formatProvider)
    {
        // for doubles, store the value of the double
        var val = Double.NaN;
        if (arg is Double)
            val = (double)arg;

        // for other types, try to convert to a double
        else
        {
            var typeConverter = TypeDescriptor.GetConverter(arg);
            if (typeConverter.CanConvertTo(typeof(Double)))
            {
                try
                {
                    val = (double)typeConverter.ConvertTo(arg, typeof(Double));
                }
                catch
                {
                    // ignore
                }
            }

            // if cannot convert, return a default value
            if(Double.IsNaN(val))
                return arg == null ? String.Empty : arg.ToString();
        }
                
        // for special cases, just write out the string
        if (val == 0.0 || Double.IsNaN(val) || Double.IsInfinity(val))
            return val.ToString();

        else
        {
            // calculate the exponents, as a power of 3
            var exp = Math.Log10(Math.Abs(val));
            var exp3 = (int)(Math.Floor(exp / 3.0) * 3.0);

            // calculate the coefficient
            var coef = val / Math.Pow(10, exp3);

            // special case, for example 0.3142
            if(exp3 == -3 && Math.Abs(coef / 1000.0) < 1 && Math.Abs(coef / 1000.0) > 0.1)
                return String.Format("{0:G4}", val);

            // for "small" numbers
            if(exp3 <= -6)
                return String.Format("{0:G4}E{1}{2:00}", coef, exp3 > 0 ? "+" : "", exp3);

            // for "large" numbers
            if(exp >= 6)
                return String.Format("{0:G4}E{1}{2:00}", coef, exp3 > 0 ? "+" : "", exp3);

            // for numbers needing thousand separators
            if (exp >= 3)
                return String.Format("{0:N0}", val);

            // default
            return String.Format("{0:G4}", val);
        }
    }
}

Here is how to use it:

var p = new EngineeringFormatProvider();
var s = String.Format(p, "{0}", number);

No comments:

Post a Comment