Created
November 21, 2025 00:46
-
-
Save IISResetMe/e24b6e17b6e46e3ce8e3aa94d45ae2f0 to your computer and use it in GitHub Desktop.
String.Format emulator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ## | |
| ## regex-based String.Format emulator | |
| ## | |
| ## In order to better understand how String.Format (or it's corresponding PowerShell operator `-f`), it might be useful to | |
| ## attempt to implement it from (relative) scratch. | |
| ## | |
| ## String.Format composite templates consist of verbatim string contents interspersed with the following macro syntax: | |
| ## | |
| ## "{" <index>["," <width>][":" <formatString>] "}" | |
| ## | |
| ## Where: | |
| ## - <index> is a non-negative integer describing the index of the $item value in the argument list | |
| ## - <width> is an integer describing the minimum length of the resulting formatted substring, negative for right-padding, positive for left-padding | |
| ## - <formatString> is a string describing the formatting parameters to be passed to ([IFormattable]$item).ToString | |
| ## | |
| ## see https://learn.microsoft.com/en-us/dotnet/standard/base-types/composite-formatting#composite-format-string | |
| ## | |
| ## The implementation in the dotnet runtime uses a low-overhead char scanner to parse template strings, but we | |
| ## can emulate the correct parsing and substition behavior using Regex.Replace in PowerShell. | |
| ## | |
| function Format-StringCustom { | |
| param( | |
| [Parameter(Mandatory, Position = 0)] | |
| [string]$Format, | |
| [Parameter(ValueFromRemainingArguments, Position = 1)] | |
| [Alias('ArgumentList')] | |
| [psobject[]]$Items, | |
| [System.IFormatProvider]$Provider = $($null) | |
| ) | |
| $formatSpecifierPattern = @' | |
| (?x) | |
| # short form: | |
| # $formatSpecifierPattern = '\{(?<index>\d+)(?:,(?<width>-?\d+))?(?::(?<formatString>[^\}]+))?\}' | |
| \{ # literal left curly | |
| (?<index> # named capture group "index" | |
| [0-9]+ ) # ... consisting of one or more digits | |
| (?: # non-capturing group #1 | |
| , # literal comma | |
| (?<width> # named capture group "width" | |
| -?[0-9]+ ) # ... consisting of optional hyphen followed by one or more digits | |
| )? # group #1 is optional | |
| (?: # non-capturing group #2 | |
| : # literal colon | |
| (?<formatString> # named capture group "formatString" | |
| [^\}]+ ) # ... consisting of 1 or more of any character that is NOT a right curly | |
| )? # group #2 is optional | |
| \} # literal right curly | |
| '@ | |
| # use the regex pattern describe above to replace all `{index[,width[:formatString]]}` substrings, resolve formatted item in match evaluator | |
| return [regex]::Replace($format, $formatSpecifierPattern, [System.Text.RegularExpressions.MatchEvaluator] { | |
| param( | |
| [System.Text.RegularExpressions.Match] | |
| $Match | |
| ) | |
| $groups = $Match.Groups | |
| # grab index value to resolve item in argument list | |
| $indexValue = +$groups['index'].Value | |
| if ($indexValue -ge $items.Count) { | |
| throw [System.FormatException]::new("Index '${indexValue}' exceeds item list count") | |
| } | |
| $itemValue = $items[$indexValue] | |
| # apply formatString if applicable, otherwise fallback to default string representation | |
| if ($groups['formatString'].Success -and $itemValue -is [System.IFormattable]) { | |
| $string = $itemValue.ToString($groups['formatString'].Value, $Provider) | |
| } | |
| else { | |
| $string = "$itemValue" | |
| } | |
| # check for width specifier, pad if applicable | |
| if ($groups['width'].Success) { | |
| # calculate absolute min-width from capture group value | |
| $absWidth = [System.Math]::Abs($groups['width'].Value) | |
| # negative width? right-pad! | |
| if ($groups['width'].Value -like '-*') { | |
| $string = $string.PadRight($absWidth) | |
| } | |
| else { | |
| $string = $string.PadLeft($absWidth) | |
| } | |
| } | |
| return $string | |
| }) | |
| } | |
| # demo class implementing IFormattable | |
| class FormattableClass : IFormattable { | |
| hidden [string]$__value | |
| FormattableClass([string]$value) { | |
| $this.__value = $value | |
| } | |
| [string] | |
| ToString([string]$format) { return $this.ToString($format, $null) } | |
| [string] | |
| ToString([string]$format, [IFormatProvider]$provider) { | |
| if ($format -ceq 'U') { return $this.__value.ToUpper() } | |
| if ($format -ceq 'L') { return $this.__value.ToLower() } | |
| return $this.__value | |
| } | |
| } | |
| # demo code | |
| $h = [FormattableClass]'Hello, World!' | |
| Write-Host (Format-StringCustom "{0,18:U}`n{0,-18:L}" $h) | |
| ## | |
| ## outputs: | |
| ## | |
| ## >>> HELLO, WORLD!<<< | |
| ## >>>hello, world! <<< | |
| ## | |
| ## Compare with String.Format: | |
| ## | |
| ## "{0,18:U}`n{0,-18:L}" -f $h |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment