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);

Tuesday, May 18, 2010

Closing a window or dialog box when an escape key is pressed in WPF

When you pop up a dialog box, users expect to be able to press the ESC key to close the dialog box. It can be quite annoying otherwise. In WPF, the easiest way to do that is to set the IsCancel property of your close button like this:

<Button Content="Close" Click="OnClose" IsCancel="True" />

On a recent project, however, I did not have a close button to set IsCancel on. The solution is to use WPF commands and input gestures. Here is an example:

<Window.CommandBindings>
   <CommandBinding Command="Close" Executed="OnCloseCmdExecuted" />
</Window.CommandBindings>

<Window.InputBindings>
   <KeyBinding Command="Close" Key="Escape" />
</Window.InputBindings>

private void OnCloseCmdExecuted(object sender, ExecutedRoutedEventArgs e)
{
   this.Close();
}