Understanding String Formatting
Padding & Alignment
Composite formatting syntax supports three semantic for
- the padding length of each interpolation
- the direction of each padding for interpolation
- the format of each interpolation
What's padding? It fills out a string to a specified length using spaces. If the specified length is less than the length of the string, string remains the same. You can specify the direction to be left or right.
// pad spaces on left
"123".PadLeft(20);
// 123
2
3
The second syntax in composite formatting is a optional integer for the interpolation:
- specify direction of padding by
-
(leave interpolated on left) and pad on right by default - length of padding
string.Format("{0,20}", 123);
// 123
string.Format("{0,-20:G}", 123);
// 123 ^ ends here
2
3
4
Format Convention
Numeric Format
G
for GeneralC
for Currency with decimal precision supportedB
for Binary numericD
for padding integers to specified trailing digit countE
for Exponential formate
for lowercase,1.23e+02
for example.
F
for Fixed-point numericN
for Numeric- formats to
ddd,ddd.ddd...
, trailing precision specifier is allowed
- formats to
P
for PercentageX
for heXadecimalx
for lowercase hexadecimal- trailing number after
X
orx
left pads to length with0
R
for Round-trip- supported for
Double
,Single
,Half
andBigInteger
only. - ensures the converted string represents the exact precision of the number.
- supported for
Arbitrary Numeric Format Composition
Composite formatting supports a dedicated syntax to represent any numeric format by following convention
0
to fill the unreached length#
to represent a single digit, does not fill up any or throw error when the#
does not match the digitscsdouble foo = 123.456; // shorter before decimal point and longer after // but still the same foo.ToString("##.#####################"); // 123.456 // rounded foo.ToString("###.##"); // 123.46 // if the format does not match the numeric(no decimal point here), will not preceed after foo.ToString("##,##"); // 123
1
2
3
4
5
6
7
8.
to represent decimal point,
as group separator, real representation depends onNumberFormatInfo.NumberGroupSeparator
, separated byNumberFormatInfo.NumberGroupSizes
%
multiply the numeric with 100 and convert it to localized string‰
multiply the numeric with 1000 and convert it to localized string- exponential format fits
[eE][+-]?0+
, see: documentation ;
to represent conditional solution for negative, zero and positive numeric within on formatcs// first section for positive value // second section for negative value // third section for zero string fmt = "+#.#;-#.#;I am zero"; 123.ToString(fmt); // +123 (-123).ToString(fmt); // -123 0.ToString(fmt); // I am zero
1
2
3
4
5
6
7\
to escape any special character above
DateTime Format
NOTE
TimeSpan Format
NOTE
Enum Format
Enum formatting is handled by Enum.ToString
static methods. They're implicitly called as if they're intrinsic.
There's two scenarios of enum formatting
- singular value A valid enum value as integer can be directly evaluated as the enum name. More specifically, the format is General when there's no format specified. If an integer is not a valid value for the enum, compiler does not yell but the default evaluation for it will be the number itselfcsHowever,
Console.WriteLine(((DayOfWeek)0).ToString()); // Sunday Console.WriteLine(((DayOfWeek)0).ToString("G")); // Sunday Console.WriteLine(((DayOfWeek)7).ToString()); // 7, which is not a member of the enum
1
2
3Enum.ToString
supportsF
format for a invalid value of enum that can be formatted as a unionNOTE
Respectively you can use
D
andX
to enforce format a enum or enum union to be decimal numeric and hexadecimal when the enum is a valid flagcsConsole.WriteLine(((DayOfWeek)7).ToString("F")); // Monday, Saturday Console.WriteLine((Foo.Bar | Foo.Baz).ToString("F")); // Bar, Baz enum Foo { None = 0b0000, Bar = 0b0001, Baz = 0b0010, Qux = 0b0100, Goo = 0b1000, All = 0b1111 }
1
2
3
4
5
6
7
8
9
10
11 - bitwise or As long as the enum was marked with
FlagAttribute
and all member values are in a powered order, the bitwise or result of any distinct combination can be formatted as the names of enum member separated by comma.csvar foo = (Foo.Bar | Foo.Baz | Foo.Bar).ToString(); // Bar, Baz [Flags] enum Foo { None = 0b0000, Bar = 0b0001, Baz = 0b0010, Qux = 0b0100, Goo = 0b1000, All = 0b1111 }
1
2
3
4
5
6
7
8
9
10
11
Custom Formatting
Before we implement a custom process for our format, we have to understand the common interfaces for formatting.
IFormatProvider
System.IFormatProvider
acts like a wrapper to cover generic solution for formatting. The return type is object
which means the format to be returned here can be any kind of representation, the use is really dependent on the method that uses the IFormatProvider
. The format object returned may contain some culture-related information, such as negative sign for numerics. And the object is usually a IFormatProvider
too.
public interface IFormatProvider
{
object? GetFormat(Type? formatType);
}
2
3
4
The parameter is the type of the type should handle the format so we can return different formatting solution for different kinds of values. That is to say, we commonly have a conditional statement inside the implementation of IFormatProvider.GetFormat
.
CultureInfo
is typically a IFormatProvider
that hanles numeric and datetime in IFormatProvider.GetFormat(Type? type)
// implementation in CultureInfo
public virtual object? GetFormat(Type? formatType)
{
if (formatType == typeof(NumberFormatInfo))
{
return NumberFormat;
}
if (formatType == typeof(DateTimeFormatInfo))
{
return DateTimeFormat;
}
return null;
}
// where NumberFormat is a process to generate a NumerFormatInfo based on Culture
public virtual NumberFormatInfo NumberFormat
{
get
{
if (_numInfo == null)
{
NumberFormatInfo temp = new NumberFormatInfo(_cultureData);
temp._isReadOnly = _isReadOnly;
Interlocked.CompareExchange(ref _numInfo, temp, null);
}
return _numInfo!;
}
set
{
ArgumentNullException.ThrowIfNull(value);
VerifyWritable();
_numInfo = value;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
The actual usage of GetFormat
inside the caller method is like
var provider = new CultureInfo("en-US");
var format = (NumberFormatInfo)provider.GetFormat(typeof(NumberFormatInfo));
2
It's kind of strange that you already know the type of format but still, it's just an identification on what kind of the handler should be returned. And the Type
should be the optimal solution since we don't know what would be formatted anyway, so we can't say there can here a enumeration as parameter.
ICustomFormatter
Implementing ICustomFormatter
means the type can handle formatting for a single value as a external handler
format
: the format for the valuearg
: the valueformatProvider
: provider for formatting
public interface ICustomFormatter
{
string Format(string? format, object? arg, IFormatProvider? formatProvider);
}
2
3
4
We always implement both IFormatProvider
and ICustomFormatter
if you want to customize your own format for any type(even existing types since ICustomFormatter
has higher priority) That is because composite formatting methods only accepts IFormatProvider
as an variant supplier, it's a good practice to do it in a same type. And the identity as a ICustomFormatter
should always be provided from IFormatProvider.GetFormat
The way to retrieve a ICustomFormatter
inside a composite formatting method is like
ICustomFormatter? cf = (ICustomFormatter?)provider?.GetFormat(typeof(ICustomFormatter));
// .. a super long process to parse the whole format string
if (cf != null)
{
s = cf.Format(itemFormat, arg, provider);
}
2
3
4
5
6
NOTE
typeof(ICustomFormatter)
is the only possible identification here, because it's a custom, external way.
While in the implementation side, the ICustomFormatter
should be returned in IFormatProvider.GetFormat
just like
class CustomFormatter : IFormatProvider, ICustomFormatter
{
public object GetFormat(Type? formatType)
{
if (formatType == typeof(ICustomFormatter))
return this;
else
{
// ... handle other types
}
}
public string Format(string? format, object? arg, IFormatProvider? formatProvider)
{
Type? type = arg?.GetType();
if (type == typeof(long))
{
//...
}
else if (type == typeof(int))
{
// ...
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
IFormattable
Implementing IFormattable
means the type itself can handle the formatting for the value it represents.
format
: the format for the valueformatProvider
: provider used for the formatting
public interface IFormattable
{
string ToString(string? format, IFormatProvider? formatProvider);
}
2
3
4
class CustomObject : IFormattable
{
public string ToString(string format, IFormatProvider? provider)
{
if (String.IsNullOrEmpty(format)) format = "G"; // use G as general
provider ??= CultureInfo.CurrentCulture;
switch (format.ToUpperInvariant())
{
case "G":
case "C":
case "F":
case "K":
default:
throw new FormatException(string.Format("The {0} format string is not supported.", format));
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Formatting Strategy
We already knew that the approaches how dotnet handles formatting for builtin types and custom types. Those solutions are all tried on methods like string.Format
with following order.
- If the value to be formatted is
null
, returnsstring.Empty
- If the
IFormatProvider
secified isICustomFormatter
,ICustomFormatter.Format(string? fmt, IFormatProvider? fmtProvider)
would be called- If
ICustomFormatter.Format
returnsnull
for current value, steps into next solution.
- If
- If
IFormatProvider
is specified- If the value is
IFormattable
,IFormattable.ToString(string fmt, IFormatProvider? fmtProvider)
is called
- If the value is
object.ToString
or overrided version was called if all approaches above are failed.
ICustomFormatter? cf = (ICustomFormatter?)provider?.GetFormat(typeof(ICustomFormatter));
string? s = null;
if (cf != null)
{
if (!itemFormatSpan.IsEmpty)
{
itemFormat = new string(itemFormatSpan);
}
s = cf.Format(itemFormat, arg, provider);
}
if (s == null) // if ICustomFormatter.Format returns null
{
// If arg is ISpanFormattable and the beginning doesn't need padding,
// try formatting it into the remaining current chunk.
if ((leftJustify || width == 0) &&
arg is ISpanFormattable spanFormattableArg &&
spanFormattableArg.TryFormat(_chars.Slice(_pos), out int charsWritten, itemFormatSpan, provider))
{
// ..
}
if (arg is IFormattable formattableArg)
{
if (itemFormatSpan.Length != 0)
{
itemFormat ??= new string(itemFormatSpan);
}
s = formattableArg.ToString(itemFormat, provider);
}
else
{
s = arg?.ToString(); // object.ToString as the last resort
}
s ??= string.Empty; // if all solution were tried but still null
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
NOTE
ISpanFormattable
is a more advance topic since .NET 6.
Formatting in Interpolated String
Use Culture
Interpolated strings are always formatted using CultureInfo.CurrentCulture
by default.
.NET 5 and earlier:
FormattableString
can be used as a wrapper of a interpolated string and useFormattableString.ToString(IFormatProvider)
to format it by certain culture.csFormattableString str = $"I am a syntax sugar for {"FormattableString"}"; _ = str.ToString(CultureInfo.CreateSpecificCulture("en-US"));
1
2since .NET 6: using
string.Create(IFormatProvider? provider, ref DefaultInterpolatedStringHandler handler)
is a more performance solution. The syntax shorthand is kind of extreme, you don't even have to specifyref
, compiler will do it for you.cs_ = string.Create( CultureInfo.CreateSpecificCulture("en-US"), $"I am a syntax sugar for {"DefaultInterpolatedStringHandler"}" );
1
2
3
4