@@ -51,7 +51,7 @@ string.Format("{0,-20}", 123);
51
51
- supported for ` Double ` , ` Single ` , ` Half ` and ` BigInteger ` only.
52
52
- ensures the converted string represents the exact precision of the number.
53
53
54
- #### Arbitrary Format Composition
54
+ ### Arbitrary Numeric Format Composition
55
55
56
56
Composite formatting supports a dedicated syntax to represent any numeric format by following convention
57
57
@@ -84,7 +84,239 @@ Composite formatting supports a dedicated syntax to represent any numeric format
84
84
```
85
85
- `\` to escape any special character above
86
86
87
- ## `ToString` & `IFormattable`
87
+ ### DateTime Format
88
88
89
+ > [! NOTE ]
90
+ > See [standard datetime format ](https :// learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings) and [Arbitrary datetime format](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings)
91
+
92
+ ### TimeSpan Format
93
+
94
+ > [! NOTE ]
95
+ > See [standard TimeSpan format ](https :// learn.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings#the-general-long-g-format-specifier) and [Arbitrary TimeSpan format](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-timespan-format-strings)
96
+
97
+ ## How to Support a Custom Format
98
+
99
+ Before we implement a custom process for our format , we have to understand the common interfaces for formatting .
100
+
101
+ ### `IFormatProvider`
102
+
103
+ `System .IFormatProvider ` acts like a wrapper to cover generic solution for formatting .
104
+ 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 `.
105
+ The *format * object returned may contain some **culture -related information **, such as negative sign for numerics . And the object is usually a `IFormatProvider ` too .
106
+
107
+ ```cs
108
+ public interface IFormatProvider
109
+ {
110
+ object ? GetFormat (Type ? formatType );
111
+ }
112
+ ```
113
+
114
+ The parameter is the type of the type should handle the *format * so we can return different formatting solution for different kinds of values .
115
+ That is to say , we commonly have a conditional statement inside the implementation of `IFormatProvider .GetFormat `.
116
+
117
+ `CultureInfo ` is typically a `IFormatProvider ` that hanles numeric and datetime in `IFormatProvider .GetFormat (Type ? type )`
118
+
119
+ ```cs
120
+ // implementation in CultureInfo
121
+ public virtual object ? GetFormat (Type ? formatType )
122
+ {
123
+ if (formatType == typeof (NumberFormatInfo ))
124
+ {
125
+ return NumberFormat ; // [!code highlight]
126
+ }
127
+ if (formatType == typeof (DateTimeFormatInfo ))
128
+ {
129
+ return DateTimeFormat ;
130
+ }
131
+
132
+ return null ;
133
+ }
134
+
135
+ // where NumberFormat is a process to generate a NumerFormatInfo based on Culture
136
+ public virtual NumberFormatInfo NumberFormat
137
+ {
138
+ get
139
+ {
140
+ if (_numInfo == null )
141
+ {
142
+ NumberFormatInfo temp = new NumberFormatInfo (_cultureData ); // [!code highlight]
143
+ temp ._isReadOnly = _isReadOnly ;
144
+ Interlocked .CompareExchange (ref _numInfo , temp , null );
145
+ }
146
+ return _numInfo ! ;
147
+ }
148
+ set
149
+ {
150
+ ArgumentNullException .ThrowIfNull (value );
151
+
152
+ VerifyWritable ();
153
+ _numInfo = value ;
154
+ }
155
+ }
156
+ ```
157
+
158
+ The actual usage of `GetFormat ` inside the caller method is like
159
+
160
+ ```cs
161
+ var provider = new CultureInfo (" en-US" );
162
+ var format = (NumberFormatInfo )provider .GetFormat (typeof (NumberFormatInfo ));
163
+ ```
164
+
165
+ 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 .
166
+ 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 .
167
+
168
+ ### `ICustomFormatter`
169
+
170
+ Implementing `ICustomFormatter ` means the type can handle formatting for a single value as a external handler
171
+
172
+ - `format `: the format for the value
173
+ - `arg `: the value
174
+ - `formatProvider `: provider for formatting
175
+
176
+ ```cs
177
+ public interface ICustomFormatter
178
+ {
179
+ string Format (string ? format , object ? arg , IFormatProvider ? formatProvider );
180
+ }
181
+ ```
182
+
183
+ 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 )
184
+ ** 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.
185
+ And the identity as a `ICustomFormatter ` should always be provided from `IFormatProvider .GetFormat `
186
+
187
+ The way to retrieve a `ICustomFormatter ` inside a composite formatting method is like
188
+
189
+ ```cs
190
+ ICustomFormatter ? cf = (ICustomFormatter ?)provider ? .GetFormat (typeof (ICustomFormatter )); // [!code highlight]
191
+ // .. a super long process to parse the whole format string
192
+ if (cf != null )
193
+ {
194
+ s = cf .Format (itemFormat , arg , provider ); // [!code highlight]
195
+ }
196
+ ```
197
+
198
+ > [! NOTE ]
199
+ > `typeof (ICustomFormatter )` is the only possible identification here , because it 's a custom, external way.
200
+
201
+ While in the implementation side , the `ICustomFormatter ` should be returned in `IFormatProvider .GetFormat ` just like
202
+
203
+ ```cs
204
+ class CustomFormatter : IFormatProvider , ICustomFormatter
205
+ {
206
+ public object GetFormat (Type ? formatType )
207
+ {
208
+ if (formatType == typeof (ICustomFormatter ))
209
+ return this ; // [!code highlight]
210
+ else
211
+ {
212
+ // ... handle other types
213
+ }
214
+ }
215
+
216
+ public string Format (string ? format , object ? arg , IFormatProvider ? formatProvider )
217
+ {
218
+ Type ? type = arg ? .GetType ();
219
+ if (type == typeof (long ))
220
+ {
221
+ // ...
222
+ }
223
+ else if (type == typeof (int ))
224
+ {
225
+ // ...
226
+ }
227
+ }
228
+ }
229
+ ```
230
+
231
+ ### `IFormattable`
232
+
233
+ Implementing `IFormattable ` means the type itself can handle the formatting for the value it represents .
234
+
235
+ - `format `: the format for the value
236
+ - `formatProvider `: provider used for the formatting
237
+
238
+ ```cs
239
+ public interface IFormattable
240
+ {
241
+ string ToString (string ? format , IFormatProvider ? formatProvider );
242
+ }
243
+ ```
244
+
245
+ ```cs
246
+ class CustomObject : IFormattable
247
+ {
248
+
249
+ public string ToString (string format , IFormatProvider ? provider )
250
+ {
251
+ if (String .IsNullOrEmpty (format )) format = " G" ; // use G as general // [!code highlight]
252
+ provider ??= CultureInfo .CurrentCulture ; // [!code highlight]
253
+
254
+ switch (format .ToUpperInvariant ()) // [!code highlight]
255
+ {
256
+ case " G" :
257
+ case " C" :
258
+ case " F" :
259
+ case " K" :
260
+ default :
261
+ throw new FormatException (string .Format (" The {0} format string is not supported." , format ));
262
+ }
263
+ }
264
+ }
265
+ ```
89
266
90
267
## Formatting Strategy
268
+
269
+ We already knew that the approaches how dotnet handles formatting for builtin types and custom types .
270
+ Those solutions are all tried on methods like `string .Format ` with following order .
271
+
272
+ - If the value to be formatted is `null `, returns `string .Empty `
273
+ - If the `IFormatProvider ` secified is `ICustomFormatter `, `ICustomFormatter .Format (string ? fmt , IFormatProvider ? fmtProvider )` would be called
274
+ - If `ICustomFormatter .Format ` returns `null ` for current value , steps into next solution .
275
+ - If `IFormatProvider ` is specified
276
+ - If the value is `IFormattable `, `IFormattable .ToString (string fmt , IFormatProvider ? fmtProvider )` is called
277
+ - `object .ToString ` or overrided version was called if all approaches above are failed .
278
+
279
+ ```cs
280
+ ICustomFormatter ? cf = (ICustomFormatter ?)provider ? .GetFormat (typeof (ICustomFormatter )); // [!code highlight]
281
+
282
+ string ? s = null ;
283
+
284
+ if (cf != null )
285
+ {
286
+ if (! itemFormatSpan .IsEmpty )
287
+ {
288
+ itemFormat = new string (itemFormatSpan );
289
+ }
290
+ s = cf .Format (itemFormat , arg , provider ); // [!code highlight]
291
+ }
292
+
293
+ if (s == null ) // if ICustomFormatter.Format returns null // [!code highlight]
294
+ {
295
+ // If arg is ISpanFormattable and the beginning doesn't need padding,
296
+ // try formatting it into the remaining current chunk.
297
+ if ((leftJustify || width == 0 ) &&
298
+ arg is ISpanFormattable spanFormattableArg &&
299
+ spanFormattableArg .TryFormat (_chars .Slice (_pos ), out int charsWritten , itemFormatSpan , provider ))
300
+ {
301
+ // ..
302
+ }
303
+
304
+ if (arg is IFormattable formattableArg ) // [!code highlight]
305
+ {
306
+ if (itemFormatSpan .Length != 0 )
307
+ {
308
+ itemFormat ??= new string (itemFormatSpan );
309
+ }
310
+ s = formattableArg .ToString (itemFormat , provider ); // [!code highlight]
311
+ }
312
+ else
313
+ {
314
+ s = arg ? .ToString (); // object.ToString as the last resort // [!code highlight]
315
+ }
316
+
317
+ s ??= string .Empty ; // if all solution were tried but still null // [!code highlight]
318
+ }
319
+ ```
320
+
321
+ > [! NOTE ]
322
+ > `ISpanFormattable ` is a more advance topic since .NET 6.
0 commit comments