How can I pad a series of hyphen-separated numbers each to two digits?

133 Views Asked by At

I am fairly new to PowerShell programming, so need help with set of strings I have as described below:

"14-2-1-1"
"14-2-1-1-1"
"14-2-1-1-10"

I want to pad zero to each number in between - if that number is between 1 and 9. So the result should look like:

"14-02-01-01"
"14-02-01-01-01"
"14-02-01-01-10"

I came up with the following code but was wondering if there is a better/faster solution.

$Filenum = "14-2-1-1"
$hicount = ($Filenum.ToCharArray() | Where-Object{$_ -eq '-'} | Measure-Object).Count
$FileNPad = ''
For ($i=0; $i -le $hicount; $i++) {
    $Filesec= "{$i}" -f $Filenum.split('-')
    If ([int]$Filesec -le 9)
    {
        $FileNPad = "$FileNPad-"+"0"+"$Filesec"
    }
    Else
    {
        $FileNPad="$FileNPad-$Filesec"
    }
}
$FileNPad = $FileNPad.Trim("-"," ")
2

There are 2 best solutions below

1
On BEST ANSWER

Instead of trying to manually keep track of how many elements and inspect each value, you can simply split on -, padleft, then join back together with -

"14-2-1-1","14-2-1-1-1","14-2-1-1-10" | ForEach-Object {
    ($_ -split '-').PadLeft(2,'0') -join '-'
}

Which outputs

14-02-01-01
14-02-01-01-01
14-02-01-01-10
1
On

I'd be inclined to go with something like Doug Maurer's answer due to its clarity, but here's another way to look at this. The last section below shows a solution that might be just as clear with some advantages of its own.

Pattern of digit groups in the input

Your input strings are composed of one or more groups, where each group...

  1. ...contains one or more digits, and...
  2. ...is preceded by a - or the beginning of the string, and...
  3. ...is followed by a - or the end of the string.

Groups that require a leading "0" to be inserted contain exactly one digit; that is, they consist of...

  1. ...a - or the beginning of the string, followed by...
  2. ...a single digit, followed by...
  3. ...a - or the end of the string.

Replacing single-digit digit groups using the -replace operator

We can use regular expressions with the -replace operator to locate that pattern and replace the single digit with a "0" followed by that same digit...

'0-0-0-0', '00-00-00-00', '1-2-3-4', '01-02-03-04', '10-20-30-40', '11-22-33-44' |
    ForEach-Object -Process { $_ -replace '(?<=-|^)(\d)(?=-|$)', '0$1' }

...which outputs...

00-00-00-00
00-00-00-00
01-02-03-04
01-02-03-04
10-20-30-40
11-22-33-44

As the documentation describes, the -replace operator is used like this...

<input> -replace <regular-expression>, <substitute>

The match pattern

The match pattern '(?<=-|^)(\d)(?=-|$)' means...

The replacement pattern

The replacement pattern '0$1' means...

  • The literal text '0', followed by...
  • The value of the first capture ((\d))

Replacing single-digit digit groups using [Regex]::Replace() and a replacement [String]

Instead of the -replace operator you can also call the static Replace() method of the [Regex] class...

'0-0-0-0', '00-00-00-00', '1-2-3-4', '01-02-03-04', '10-20-30-40', '11-22-33-44' |
    ForEach-Object -Process { [Regex]::Replace($_, '(?<=-|^)(\d)(?=-|$)', '0$1') }

...and the result is the same.

Replacing digit groups using [Regex]::Replace() and a [MatchEvaluator]

A hybrid of the regular expression and imperative solutions is to call an overload of the Replace() method that takes a [MatchEvaluator] instead of a replacement [String]...

# This [ScriptBlock] will be passed to a [System.Text.RegularExpressions.MatchEvaluator] parameter
$matchEvaluator = {
    # The [System.Text.RegularExpressions.Match] parameter
    param($match)

    # The replacement [String]
    return $match.Value.PadLeft(2, '0')
}

'0-0-0-0', '00-00-00-00', '1-2-3-4', '01-02-03-04', '10-20-30-40', '11-22-33-44' |
    ForEach-Object -Process { [Regex]::Replace($_, '(\d+)', $matchEvaluator) }

This produces the same result as above.

A [MatchEvaluator] is a delegate that takes a Match to be replaced ($match) and returns the [String] with which to replace it (the matched text left-padded to two digits). Also note that, whereas above we are capturing only standalone digits (\d), here we capture all groups of one or more digits (\d+) and leave it to PadLeft() to figure out if a leading "0" is needed.

I think this is a much more compelling solution than regular expressions alone because it's the best of that and the imperative world:

  • It uses a simple regular expression pattern to locate digit groups in the input string
  • It uses a simple [ScriptBlock] to transform digit groups in the input string
  • By not splitting the input string apart it does not create as much intermediate string and array garbage
    • Whether this potential performance improvement is overshadowed by using regular expressions at all, I can't say without benchmarking