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);
// 1232
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 here2
3
4
Format Convention β
Numeric Format β
Gfor GeneralCfor Currency with decimal precision supportedBfor Binary numericDfor padding integers to specified trailing digit countEfor Exponential formatefor lowercase,1.23e+02for example.
Ffor Fixed-point numericNfor Numeric- formats to
ddd,ddd.ddd..., trailing precision specifier is allowed
- formats to
Pfor PercentageXfor heXadecimalxfor lowercase hexadecimal- trailing number after
Xorxleft pads to length with0
Rfor Round-trip- supported for
Double,Single,HalfandBigIntegeronly. - 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
0to 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("##,##"); // 1231
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 zero1
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 itself
csConsole.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 enum1
2
3However,
Enum.ToStringsupportsFformat for a invalid value of enum that can be formatted as a unionNOTE
Respectively you can use
DandXto 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
10bitwise or As long as the enum was marked with
FlagAttributeand 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
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
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
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
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
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
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
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
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
IFormatProvidersecified isICustomFormatter,ICustomFormatter.Format(string? fmt, IFormatProvider? fmtProvider)would be called- If
ICustomFormatter.Formatreturnsnullfor current value, steps into next solution.
- If
- If
IFormatProvideris specified- If the value is
IFormattable,IFormattable.ToString(string fmt, IFormatProvider? fmtProvider)is called
- If the value is
object.ToStringor 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
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:
FormattableStringcan 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