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:
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?
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; } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With