Detecting Digits with PowerShell

A person can look at a number and determine which digits are present. How would a script achieve this?

James Montgomery

12 minute read

TL; DR

Have you ever wondered, how many unique digits are in a number? Alternatively, how many times does a given digit appear in a number? Using PowerShell, I explore these questions in a variety of means and examples.

How to think about the problem

What is a digit?

A numerical digit in the decimal system is:

  • 0-9.
  • The integer representation of the characters ‘0’-‘9’, ‘0’ is zero, ‘1’ is one, ‘2’ is two and so on.
  • Not a non-digit (see below).
  • Greater than or equal to 0. But less than 10.

A non-digit is describable as:

  • Not an alphabetic character (‘a’-‘z’,‘A-Z’) in the case of the Latin alphabet.
  • Not a symbol (e.g. ‘&‘,’%,“$‘) or other special character.

A number is a combination of digits, for example, 1765.

Working with a number in PowerShell

We can work with the number natively, or we can convert it into another data type. Treating our input as a String object opens many avenues. However, some are more straightforward than others.

[System.Math]::DivRem

Using mathematics to answer the question is interesting in its own right. I was inspired by this thread to use DivRem for a timeless solution.

Whenever you divide a decimal integer by 10, the remainder is the rightmost digit.

2698 / 10 = 269 with a remainder of 8.
269 / 10 = 26 with a remainder of 9.
26 / 10 = 2 with a remainder of 6.
2 / 10 = 0 with a remainder of 2.

Using DivRem to detect a digit

In the below example we:

  • Generate a random number as our digit of interest.
  • Generate another random number to search in.
  • Divide by 10 the input number, evaluating the remainder each time.
  • Repeat the division with the result until we reach zero or have found our number.
#Digit of interest
$lookingFor = Get-Random -Minimum 0 -Maximum 9
#Initialise found to false
$fountInt = $false

#Generate our desired input number
[int64]$inputNumber = Get-Random  -Minimum 1111111111 -Maximum 9999999999
[int64]$remainderFromDivision = 0

Write-host "Generated: $inputNumber, Looking for: $lookingFor"

# Division by 10 until the inputNumber gets down to 0 unless we find our desired digit
do
{
    #Using a division with remain function. Divide by 10 and retrieving the remainder via a reference
    $inputNumber = [System.Math]::DivRem($inputNumber, 10, [ref]$remainderFromDivision)
    write-host "Processing: $remainderFromDivision"
    #remainder evaluated
    if($remainderFromDivision -eq $lookingFor){
        $fountInt = $true
    }elseif ($inputNumber -gt 0)
    {
        "Continue looking in $inputNumber"
    }

} while ($inputNumber -gt 0 -and $fountInt -eq $false)

#Print the outcome
Write-host "Found digit: $fountInt"

Example output for the above:

Generated: 2074723921, Looking for: 0
Processing: 1
Continue looking in 207472392
Processing: 2
Continue looking in 20747239
Processing: 9
Continue looking in 2074723
Processing: 3
Continue looking in 207472
Processing: 2
Continue looking in 20747
Processing: 7
Continue looking in 2074
Processing: 4
Continue looking in 207
Processing: 7
Continue looking in 20
Processing: 0
Found digit: True

You may note the use of [int64] above and wonder why.

Our random number generator specifies a maximum value of 9999999999. The maximum value of [int32] confirmed via [int32]::MaxValue is 2147483647. The result could exceed 2147483647 and would result in the data type for $inputNumber sometimes being [int32] and other times [int64].

Less obvious is the use of [int64] for $remainderFromDivision especially as the value (0-9) fits comfortably inside a smaller integer data type such as [int32] (the default for this value range).

The issue arises with the definition of DivRem itself.

DivRem(Int32, Int32, Int32)
DivRem(Int64, Int64, Int64)

It does not support mixed 32-bit and 64-bit input. We prevent this from becoming an issue by explicitly casting our variables to 64-bit.

Otherwise, PowerShell’s automatic type assignment could result in a call to an undefined overload of DivRem such as DivRem(Int32, Int64, Int64) or DivRem(Int64, Int64, Int32).

Our other option would be to reduce the maximum value of the random number such that it cannot generate a 64-bit integer. One way to do this would be as follows:

#Generate our desired input number
$inputNumber = Get-Random  -Minimum 1111111111 -Maximum ([int32]::MaxValue)
$remainderFromDivision = 0

Using DivRem to detect a range of digits

We expand on the above to search for unique digits. During the do-while loop, evaluation of each remainder result against an array of digits found so far occurs. If we have not found it previously, we add the digit to the array.

#Array to store unique digits found, initially empty.
$foundInts = @()

#Generate our desired input number and initialise remainder variable
[int64]$inputNumber = Get-Random  -Minimum 1111111111 -Maximum 9999999999
[int64]$remainderFromDivision = 0

Write-host "Generated: $inputNumber"

# Division by 10 until the inputNumber gets down to 0
do
{
    #Using a division with remain function. Divide by 10 and retrieving the remainder via a reference
    $inputNumber = [System.Math]::DivRem($inputNumber, 10, [ref]$remainderFromDivision)
    write-host "Processing: $remainderFromDivision"
    #remainder evaluated against the foundInts array
    if($remainderFromDivision -notin $foundInts){
        #Add to the array if not present
        $foundInts += $remainderFromDivision
    }

} while ($inputNumber -gt 0)

#Print the found integers in ascending order of value
$foundInts | Sort-Object | ForEach-Object {Write-Host "[Math-sorted]Digit $_ detected"}

The code above outputs the following result:

Generated: 8438923050
Processing: 0
Processing: 5
Processing: 0
Processing: 3
Processing: 2
Processing: 9
Processing: 8
Processing: 3
Processing: 4
Processing: 8
[Math-sorted]Digit 0 detected
[Math-sorted]Digit 2 detected
[Math-sorted]Digit 3 detected
[Math-sorted]Digit 4 detected
[Math-sorted]Digit 5 detected
[Math-sorted]Digit 8 detected
[Math-sorted]Digit 9 detected

Contains

Introducing .contains(), a native method to String which returns a boolean answer (true or false) if the input string is present.

Using .contains() to detect a digit

The contains method accepts an integer value for the search term. The result is that we only need to convert one number to a string.

To illustrate this we explictly cast our variables to the [string] and [int] data types in this example:

[string]$inputText = "60263455"
[int]$myDigit = 5
$inputText.Contains($myDigit)
#Expecting True as the result, as the digit is present.

If the original data is an integer, it is required to convert into a string. In this example the conversion is illustrated via $originalNumber.ToString():

[int]$originalNumber = 60263455
[string]$inputText = $originalNumber.ToString()
[int]$myDigit = 7
$inputText.Contains($myDigit)
#Expecting False as the result, as the digit is not present.

Using .contains() to detect a range of digits

In this example, we build a List of digits. The presence of a digit in this list is the basis of our test.

#Setup our input number as a string
$inputText = (60263455).ToString()

#Create a generic list for integers
$myLOD = [System.Collections.Generic.List[int]]::new()
#Load the list with all digits of interest. In this case we add the full range.
0 .. 9 | ForEach-Object {$myLOD.Add($_)}

#Loop through each digit in the list
foreach($digit in $myLOD){
    #Test if the original input text contains the digit
    if ($inputtext.Contains($digit))
    {
        Write-Host "[list of digits]$digit"
    }
}

The code above outputs the following result:

[list of digits]0
[list of digits]2
[list of digits]3
[list of digits]4
[list of digits]5
[list of digits]6

Match

The -match comparison operator works similarly to .contains(). Where .contains() seeks a referenced value, -match tests for the presence of pattern specified in regex form.

Using -match to detect a specific digit

You can test for a digit as follows:

[int]$inputNumber = 563
[int]$searchingFor = 7
$inputNumber -match $searchingFor
#Expecting False
[int]$inputNumber = 563
[int]$searchingFor = 5
$inputNumber -match $searchingFor
#Expecting True

The documentation for -match describes this as a string matching operator. The above example uses integers for input and matches terms. We can infer that type conversion occurs in the case of using integers.

Using -Match to detect a range of digits

The script for this is relatively compact as we can use -match in-line during the if conditional test. Use of the pipeline via 0 .. 9 | ForEach-Object and the $_ automatic variable achieves this compactness.

#Setup our input number explicitly as an Integer
[int]$inputNumber = 62419
#Iterate through the full digit range
0 .. 9 | ForEach-Object{
    #Test for this number being present. -match returns true or false.
    if($inputNumber -match "$_")
    {
        write-host "[match]$_ present"
    }
    else {
        write-host "[match]$_ not present"
    }
}

The code above produces the following output:

[match]0 not present
[match]1 present
[match]2 present
[match]3 not present
[match]4 present
[match]5 not present
[match]6 present
[match]7 not present
[match]8 not present
[match]9 present

Split

The split operator is used to produce substrings from strings based on a provided delimiter. When the input is an integer, the split operator treats the input as if it were a string, saving us from any explicit type conversion.

Using -split to detect a digit

We can count the results of the split operation to determine if the delimiter (our digit of interest) is present.

If the delimiter is present, the result is the output of two or more substrings as an array. If the delimiter is not present, the returned array contains only one item - the original input as a string.

To illustrate the point on type conversion, we are casting our variables as integers via the use of [int].

For example:

#Setup our input number and digit as integers
[int]$inputNumber = 17992347
[int]$myDigit = 9

($inputNumber -split $myDigit).count
#Expecting 3 as the result, it is present twice in the input number.

$myDigit = 2
($inputNumber -split $myDigit).count
#Expecting 2 as the result, it is present once in the input number.

$myDigit = 5
($inputNumber -split $myDigit).count
#Expecting 1 as the result, it is not present in the input number.

The number of occurrences is always one less than the count result. Therefore, we can use count -1 to determine how many times the digit is present.

Using -split to detect a range of digits

We can expand on the above to detect which digits are present or non-present. In this example, an Array is used to describe the set of numbers we wish to detect.

#Setup our input number. This will default to an integer type. Check via $inputNumber.GetType()
$inputNumber = 17239947

#Create an array of numbers we wish to detect.
#Setup the array to contain these three numbers:
$numbersToDetect = 1,7,8
#Alternatively, using the range operator, uncomment the next line to set up the array to contain a range of digits. In this case, the full range.
#$numbersToDetect = 0 .. 9

$numbersToDetect| ForEach-Object {
    $thisCount = ($inputNumber -split $_).count -1
    if($thisCount -gt 1){
        Write-Host "[Split]Digit $_ detected $thisCount times"
    }elseif ($thisCount -eq 1) {
        Write-Host "[Split]Digit $_ detected $thisCount time"
    }else{
        Write-Host "[Split]Digit $_ not detected"
    }
}

The code above outputs the following result:

[Split]Digit 1 detected 1 time
[Split]Digit 7 detected 2 times
[Split]Digit 8 not detected

Illustrated above is the potential to check for specific digits 1, 7 and 8. The next line (#$numbersToDetect = 0 .. 9) illustrates how this could be changed to check for the presence of all digits. The results of this change would be:

[Split]Digit 0 not detected
[Split]Digit 1 detected 1 time
[Split]Digit 2 detected 1 time
[Split]Digit 3 detected 1 time
[Split]Digit 4 detected 1 time
[Split]Digit 5 not detected
[Split]Digit 6 not detected
[Split]Digit 7 detected 2 times
[Split]Digit 8 not detected
[Split]Digit 9 detected 2 times

If you wished to check for the presence of the complete set without the per-digit detail, the following would suffice:

#Setup our input number. This variable defaults to an integer type. Check via $inputNumber.GetType()
$inputNumber = 172399475680

#Setup the array to contain the full range of digits
$numbersToDetect = 0 .. 9

#Setup result text and text colour if all numbers are found
$allDigitsPresent = $true
$thisColour = "Green"

#Loop through each digit in the array
$numbersToDetect | ForEach-Object {
    #Get the count of objects in the array create by the split operation and substract 1
    $thisCount = ($inputNumber -split $_).count -1
    #If any count is equal to 0, this number is missing, therefore we do not have all the numbers we are looking for.
    if($thisCount -eq 0){
        #Change the result next now that we have found a missing number
        $allDigitsPresent = $false
        $thisColour = "Red"
    }
}
#Print the result text
Write-host -ForegroundColor $thisColour "All digits found? $allDigitsPresent "

ASCII Method

The ASCII method uses the relationship between the character of a digit and it’s ASCII assigned number. For example:

The ASCII decimal value of ‘0’ is 48, ‘1’ is 49, ‘2’ is 50, and so on until we get to ‘9’ which is 57.

Whenever you perform subtraction on a Char, the numeric result is the ASCII number. For example:

[char]$thisChar = '1'
$thisChar - 1
48

The result is an integer which you could confirm with .gettype(). If we cast this back into a Char:

[char]($thisChar-1)

The result is the character ‘0’.

When you perform addition in this manner, the default behaviour is character concatenation. For example:

[char]$thisChar = '1'
$thisChar + 1

The result is the string “11”, rather than the ASCII value of 50 that you might expect.

By casting the character into an integer value, we can achieve consistent results:

[char]$thisChar = '1'
[int]$thisChar + 1
50
[char]$thisChar = '1'
[int]$thisChar - 1
48

Lastly, you can return the character output if you wish, by casting the result back to Char:

[char]$thisChar = '1'
[int]$thisInt = 1
[char]([int]$thisChar + $thisInt)
2

Using the ASCII value of a character to detect unique digits in a number

Now that we understand that the ASCII value of a digit is always 48 from its integer value, we can proceed as follows:

  • Convert the number to an array of characters
  • Get the ASCII value of each character and subtract 48. The character is a digit if the result is between 0 and 9.
  • To find all unique digits, store discovered integers as you process the characters.

An example of this:

#Setup our input number as a sting
$inputText = (1601263455).ToString()
#
$thisColour = "Yellow"

#Create empty array
$foundInts = @()

#Read every character in the array of characters created from the string $inputtext
foreach($readChar in $inputtext.ToCharArray()){
    #For each character we subtract 48 from its ASCII value
    $charAsInt = $readChar - 48
    #We test if the result is in the range 0-9
    if($charAsInt -ge 0 -And $charAsInt -lt 10){
        #If the result is in the desired range, we test if found previously.
        if($charAsInt -notin $foundInts){
            #If we haven't found the digit previously, we add it to the array.
            $foundInts += $charAsInt
            #Write which digit we have located. Digits printed in the order of detection.
            Write-Host "[ASCI]Digit $charAsInt detected"
        }
    }
}
#Print how many unique digits were found.
Write-host -ForegroundColor $thisColour $foundInts.Count "found in the string"
#Re-print the digits in ascending order.
$foundInts | Sort-Object | foreach-object {Write-host "[ASCI ordered] $_ detected"}

Conclusion

I began working on this as part of another personal project requiring digit detection. Not entirely satisfied with my initial approach, I decided to find other ways to accomplish the same.

This write-up is intended to share these and I’ve attempted to explain rationale along the way.

If you’ve other ways to do this, do share!

Acknowledgements

Other online works have assisted me directly or indirectly along the way, I’ve tried to link in-line above or below.