Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extending the custom formatting capabilities of built-in types

I have some rather awkward formatting requirements for decimal values. In a nutshell: display to two decimal places with a trailing space unless the third decimal is a 5, in which case display to three decimal places.

This formatting needs to be fairly flexible, too. Specifically, the trailing space will not always be desired, and a "½" may be preferred when the third decimal is a "5".

Examples:

  • 1.13 would be displayed as "01.13 " with a space or "01.13" without it
  • 1.315 would be displayed as "01.315" or "01.31½"

I need to use this logic consistently across otherwise unrelated pieces of UI. I have temporarily written it as a WPF value converter, but this is just for demonstration:

public sealed class PriceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!(value is decimal))
        {
            return DependencyProperty.UnsetValue;
        }

        var decimalValue = (decimal)value;
        var formattedDecimalValue = decimalValue.ToString("#0.000", CultureInfo.InvariantCulture);
        var lastFormattedChar = formattedDecimalValue[formattedDecimalValue.Length - 1];

        switch (lastFormattedChar)
        {
            case '0':
                return formattedDecimalValue.Substring(0, formattedDecimalValue.Length - 1) + " ";
            case '5':
                return formattedDecimalValue.Substring(0, formattedDecimalValue.Length - 1) + "½";
            default:
                return formattedDecimalValue;
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

I am now trying to extract this into a more fundamental building block I can use throughout my UI layer. My initial thought was a custom format provider which I could then use from a Binding:

<TextBlock Text="{Binding Value, FormatString=WHATEVER}"/>

The idea is that format string could be something like "#0.005" which indicates to only show the third decimal place if it's a 5, or "#0.00F" which attempts to represent the third decimal as a fraction. However, I was unable to find a means of using a specific format provider from a binding, which seems like a major limitation to me, but maybe I'm missing something...?

After more experimentation and investigation, I came to the conclusion that my only option is to define my own type:

public struct Price : IFormattable

This type would encapsulate the extra formatting capabilities I require. However, now I have another conundrum: in my ToString implementation, how can I leverage the existing formatting capabilities of decimal.ToString(string, IFormatProvider) without interfering with my own? It seems like this would be pretty darn messy, and it's causing me to lean towards a more limited solution of just defining "G" (two or three decimal places, no trailing space) and "S" (same as "G", but with trailing space if necessary) formats for my Price structure.

Can anyone tell me whether there's a way for me to do this kind of custom formatting capability without too much hassle?

like image 703
Kent Boogaart Avatar asked Nov 25 '13 03:11

Kent Boogaart


1 Answers

See http://msdn.microsoft.com/en-us/library/system.iformatprovider.aspx for more details.

// "01.13 " or "01.13". Standard formatting applied: $123.45 // "01.315" or "01.31½". Standard formatting applied: $123.45  public class Test {     void Main()     {         decimal number1 = 1.13M;         decimal number2 = 1.315M;          string output1 = String.Format(new CustomNumberFormat(),                                  "\"{0:G}\" or \"{0:S}\". Standard formatting applied: {1:C2}",                                  number1, 123.45);         Console.WriteLine(output1);          string output2 = String.Format(new CustomNumberFormat(),                                  "\"{0:G}\" or \"{0:S}\". Standard formatting applied: {1:C2}",                                  number2, 123.45);         Console.WriteLine(output2);     } }  public class CustomNumberFormat : System.IFormatProvider, System.ICustomFormatter {     public object GetFormat(Type formatType)     {         if (formatType == typeof(ICustomFormatter))             return this;         else             return null;     }      public string Format(string fmt, object arg, System.IFormatProvider formatProvider)     {         // Provide default formatting if arg is not a decimal.          if (arg.GetType() != typeof(decimal))             try             {                 return HandleOtherFormats(fmt, arg);             }             catch (FormatException e)             {                 throw new FormatException(String.Format("The format of '{0}' is invalid.", fmt), e);             }          // Provide default formatting for unsupported format strings.          string ufmt = fmt.ToUpper(System.Globalization.CultureInfo.InvariantCulture);         if (!(ufmt == "G" || ufmt == "S"))             try             {                 return HandleOtherFormats(fmt, arg);             }             catch (FormatException e)             {                 throw new FormatException(String.Format("The format of '{0}' is invalid.", fmt), e);             }          // Convert argument to a string.          string result = ((decimal)arg).ToString("0#.000");          if (ufmt == "G")         {             var lastFormattedChar = result[result.Length - 1];             switch (lastFormattedChar)             {                 case '0':                     result = result.Substring(0, result.Length - 1) + " ";                     break;             }              return result;         }         else if (ufmt == "S")         {             var lastFormattedChar = result[result.Length - 1];             switch (lastFormattedChar)             {                 case '0':                     result = result.Substring(0, result.Length - 1);                     break;                 case '5':                     result = result.Substring(0, result.Length - 1) + "½";                     break;             }              return result;         }         else         {             return result;         }     }      private string HandleOtherFormats(string format, object arg)     {         if (arg is System.IFormattable)             return ((System.IFormattable)arg).ToString(format, System.Globalization.CultureInfo.CurrentCulture);         else if (arg != null)             return arg.ToString();         else             return String.Empty;     } } 
like image 135
Daniel Holder Avatar answered Nov 22 '22 14:11

Daniel Holder