How can I specify a format string for a boolean that's consistent with the other format strings for other types?
Given the following code:
double d = Math.PI;
DateTime now = DateTime.Now;
bool isPartyTime = true;
string result = $"{d:0.0}, {now:HH:mm}, time to party? {isPartyTime}";
I can specify a format for every primitive type, except for bool
it seems. I know I can do:
string result = $"{d:0.0}, {now:HH:mm}, time to party? {(isPartyTime ? "yes!" : "no")}";
However this is still inconsistent with the other types.
Is there a way of formatting booleans in interpolated strings that is consistent?
P.S. I did search for an answer including this link:
https://stackoverflow.com/questions/tagged/c%23+string-interpolation+boolean
And surprisingly had zero results.
Custom String Interpolation Handlers are documented here and here
(I don't have any experience with any C# 10.0 features yet, but I'll expand this section in future - right now I'm still stuck in C# 7.3 land due to my day-job's projects' dependencies on .NET Framework 4.8)
Boolean
wrapper structIf you control the string-formatting call-sites, then just change bool
/Boolean
-typed values to use an implicitly-convertible zero-overhead value-type instead, e.g.:
public readonly struct YesNoBoolean : IEquatable<YesNoBoolean>
{
// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/user-defined-conversion-operators
public static implicit operator Boolean ( YesNoBoolean self ) => self.Value;
public static implicit operator YesNoBoolean( Boolean value ) => new MyBoolean( value );
// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/true-false-operators
public static Boolean operator true( YesNoBoolean self ) => self.Value == true;
public static Boolean operator false( YesNoBoolean self ) => self.Value == false;
public YesNoBoolean( Boolean value )
{
this.Value = value;
}
public readonly Boolean Value;
public override String ToString()
{
return this.Value ? "Yes" : "No";
}
// TODO: Override Equals, GetHashCode, IEquatable<YesNoBoolean>.Equals, etc.
}
So your example call-site becomes:
double d = Math.PI;
DateTime now = DateTime.Now;
YesNoBoolean isPartyTime = true; // <-- Yay for implicit conversion.
string result = $"{d:0.0}, {now:HH:mm}, time to party? {isPartyTime}";
And result
will be "3.1, 21:03, time to party? Yes"
Boolean.TrueString
and FalseString
Because Boolean
's static readonly String TrueString = "True";
is also marked with initonly
you cannot overwrite it using reflection, so doing this:
typeof(Boolean).GetField( "TrueString" )!.SetValue( obj: null, value: "Yes" );
...will give you a runtime exception:
Cannot set
initonly static field
'TrueString
' after type 'System.Boolean
' is initialized.
It is still possible by manipulating raw memory, but that's out-of-scope for this question.
IFormatProvider
and ICustomFormatter
:It's always been possible to override how both String.Format
and interpolated strings (e.g. $"Hello, {world}"
) are formatted by providing a custom IFormatProvider
; though while String.Format
makes it easy by exposing a Format
overload parameter, interpolated strings do not, instead it forces you to uglify your code somewhat.
IFormatProvider
is (still) surprisingly underdocumented in .NET.
IFormatProvider.GetFormat(Type)
is only ever invoked with one of these 3 formatType
arguments:
typeof(DateTimeFormatInfo)
typeof(NumberFormatInfo)
typeof(ICustomFormatter)
typeof()
types are passed into GetFormat
(at least as far as ILSpy and RedGate Reflector tell me).The magic happens inside ICustomFormatter.Format
and implementing it is straightforward:
public class MyCustomFormatProvider : IFormatProvider
{
public static readonly MyCustomFormatProvider Instance = new MyCustomFormatProvider();
public Object? GetFormat( Type? formatType )
{
if( formatType == typeof(ICustomFormatter) )
{
return MyCustomFormatter.Instance;
}
return null;
}
}
public class MyCustomFormatter : ICustomFormatter
{
public static readonly MyCustomFormatter Instance = new MyCustomFormatter();
public String? Format( String? format, Object? arg, IFormatProvider? formatProvider )
{
// * `format` is the "aaa" in "{0:aaa}"
// * `arg` is the single value
// * `formatProvider` will always be the parent instance of `MyCustomFormatProvider` and can be ignored.
if( arg is Boolean b )
{
return b ? "Yes" : "No";
}
return null; // Returning null will cause .NET's composite-string-formatting code to fall-back to test `(arg as IFormattable)?.ToString(format)` and if that fails, then just `arg.ToString()`.
}
public static MyFormat( this String format, params Object?[] args )
{
return String.Format( Instance, format: format, arg: args );
}
}
...so just pass MyCustomFormatProvider.Instance
into String.Format
somehow, like below.
double d = Math.PI;
DateTime now = DateTime.Now;
bool isPartyTime = true;
string result1 = String.Format( MyCustomFormatProvider.Instance, "{0:0.0}, {1:HH:mm}, time to party? {2}", d, now, isPartyTime );
// or add `using static MyCustomFormatProvider` and use `MyFormat` directly:
string result2 = MyFormat( "{0:0.0}, {1:HH:mm}, time to party? {2}", d, now, isPartyTime );
// or as an extension method:
string result3 = "{0:0.0} {1:HH:mm}, time to party? {2}".MyFormat( d, now, isPartyTime );
// Assert( result1 == result2 == result3 );
So that works for String.Format
, but how can we use MyCustomFormatProvider
with C# $""
interpolated strings...?
...with great difficulty, because the C# langauge team who designed the interpolated strings feature made it always pass provider: null
so all values use their default (usually Culture-specific) formatting, and they didn't provide any way to easily specify a custom IFormatProvider
, even though there's decades-old Static Code Analysis rule against relying on implicit use of CurrentCulture
(though it's not uncommon for Microsoft to break their own rules...).
CultureInfo.CurrentCulture
won't work because Boolean.ToString()
doesn't use CultureInfo
at all.The difficulty stems from the fact that C# $""
interpolated strings are always implicitly converted to String
(i.e. they're formatted immediately) unless the $""
string expression is directly assigned to a variable or parameter typed as FormattableString
or IFormattable
, but infuriatingly this does not extend to extension methods (so public static String MyFormat( this FormattableString fs, ... )
won't work.
The the only thing that can be done here is to invoke that String MyFormat( this FormattableString fs, ... )
method as a (syntactically "normal") static method call, though using using static MyFormattableStringExtensions
somewhat reduces the ergonomics problems - even more-so if you use global-usings (which requires C# 10.0, which already supports custom interpolated-string handlers, so that's kinda moot).
But like this:
public static class MyFormattableStringExtensions
{
// The `this` modifier is unnecessary, but I'm retaining it just-in-case it's eventually supported.
public static String MyFmt( this FormattableString fs )
{
return fs.ToString( MyCustomFormatProvider.Instance );
}
}
And used like this:
using static MyFormattableStringExtensions;
// ...
double d = Math.PI;
DateTime now = DateTime.Now;
bool isPartyTime = true;
string result = MyFmt( $"{d:0.0}, {now:HH:mm}, time to party? {isPartyTime}" );
Assert.AreEqual( result, "3.1, 23:05, time to party? Yes" );
FormattableString
's arguments arrayMyFmt( $"" )
above), there's a simpler alternative approach to having to implement IFormatProvider
and ICustomFormatter
: just edit the FormattableString
's value arguments array directly.Boolean
values in String.Format(IFormatProvider, String format, ...)
.public static class MyFormattableStringExtensions
{
public static String MyFmt( this FormattableString fs )
{
if( fs.ArgumentCount == 0 ) return fs.Format;
Object?[] args = fs.GetArguments();
for( Int32 i = 0; i < args.Length; i++ )
{
if( args[i] is Boolean b )
{
args[i] = b ? "Yes" : "No";
}
}
return String.Format( CultureInfo.CurrentCulture, fs.Format, arg: args );
}
}
And used just like before to get the same results:
using static MyFormattableStringExtensions;
// ...
double d = Math.PI;
DateTime now = DateTime.Now;
bool isPartyTime = true;
string result = MyFmt( $"{d:0.0}, {now:HH:mm}, time to party? {isPartyTime}" );
Assert.AreEqual( result, "3.1, 23:05, time to party? Yes" );