@@ -51,7 +51,7 @@ string.Format("{0,-20}", 123);
5151 - supported for ` Double ` , ` Single ` , ` Half ` and ` BigInteger ` only.
5252 - ensures the converted string represents the exact precision of the number.
5353
54- #### Arbitrary Format Composition
54+ ### Arbitrary Numeric Format Composition
5555
5656Composite formatting supports a dedicated syntax to represent any numeric format by following convention
5757
@@ -84,7 +84,239 @@ Composite formatting supports a dedicated syntax to represent any numeric format
8484 ```
8585- `\` to escape any special character above
8686
87- ## `ToString` & `IFormattable`
87+ ### DateTime Format
8888
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+ ```
89266
90267## 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