Last month, the Iron Scripter Chairman posted a "fun" PowerShell scripting challenge. Actually, a few math-related challenges . As with all these challenges, the techniques and concepts you use to solve the challenge are more important than the result itself. Here's how I approached the problems.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Problem #1
The first challenge was to take a number, like 2568, and get the sum total of the individual integers.
2+5+6+8
The answer being 21. You can probably think of a few ways to split the string into individual elements. I decided to use regular expressions.
[int]$i = 12931
#define a regex pattern to match a single digit
[regex]$rx="\d{1}"
Each regular expression match object value will be the individual number. I can easily pipe the values to Measure-Object to get the sum.
#get all matches and add them up
$measure = $rx.Matches($i) | Measure-Object -Property Value -Sum
#write the sum to the pipeline
$measure.sum
This is easy enough to turn into a function.
Function Get-NumberSum {
[cmdletbinding()]
[OutputType([Int])]
Param(
[Parameter(Position = 0, Mandatory, HelpMessage = "Enter an integer value like 1234")]
[Alias("i")]
[int]$Value
)
#define a regex pattern to match a single digit
[regex]$rx = "\d{1}"
#get all matches and add them up
$measure = $rx.Matches($Value) | Measure-Object -Property Value -Sum
#write the sum to the pipeline
$measure.sum
}
The function wants an integer. But the regular expression pattern works on anything with numbers in it.
($rx.matches("A12B9C31D") | measure value -sum).sum
This gives me the same answer of 16. The challenge had some bonus elements, so here is a slightly more advanced version of the function.
Function Get-NumberSum {
[cmdletbinding()]
[OutputType([Int])]
[OutputType("NumberSum")]
[Alias("gns")]
Param(
[ValidateScript({
if ($_.ToString().Length -le 10) {
$True
}
else {
Throw "Specify a value of 10 digits or less."
$False
}
})]
[int64]$Value,
[switch]$Quiet
)
Write-Verbose "Processing $Value"
#define a regex pattern to match a single digit
[regex]$rx = "\d{1}"
$values = $rx.Matches($Value).Value
Write-Verbose ($values -join "+")
$measure = $Values | Measure-Object -Sum
Write-Verbose "Equals $($measure.sum)"
if ($Quiet) {
$measure.sum
}
else {
[pscustomobject]@{
PSTypeName = "NumberSum"
Value = $Value
Elements = $values
Sum = $measure.sum
}
}
}
This version will create a custom object by default. The -Quiet parameter shows only the result. The parameter validation could be handled in a number of ways. If I kept the type for $Value as [int], the number can't be more than 10 digits anyway. But I wanted to try something different. So I'm using a ValidateScript attribute to display a custom error message. Here's what it looks like using the function alias.
Ok, I'll admit using a regular expression is a little overkill for this challenge, but it is fun. Here's an even simpler way.
($i -split "" | measure-object -sum).sum
Problem #2
The second problem was a bit more challenging. Given an array of numbers, what are all the possible unique sums.
$a = 2,5,6
2
5
6
2+5 = 7
2+5+6 = 13
2+6 = 8
5+6 = 11
This problem requires getting the sum of numbers from the array, and taking all possible combinations into account. The trick is to recursively call the function, reducing the numbers to test in combination.
function Get-ReductiveSum {
param([array]$Numbers,[int]$Index=0,[int]$Sum=0)
if ($numbers.count -eq $index) {
$sum
}
else {
Get-ReductiveSum $numbers -index ($index+1) -sum ($sum+$numbers[$index])
Get-ReductiveSum $numbers ($index+1) $sum
}
}
I can use the function like this.
To meet the challenge objectives and make this easy to use, I created this "parent" function.
Function Get-PossibleSum {
[cmdletbinding()]
Param(
[Parameter(Position = 0, Mandatory)]
[ValidateRange(1,9)]
[int[]]$Values
)
#nested function
function Get-ReductiveSum {
param([array]$Numbers,[int]$Index=0,[int]$Sum=0)
Write-Verbose "Get-ReductiveSum -Numbers $($numbers -join ',') -index $index -sum $sum"
if ($numbers.count -eq $index) {
Write-Verbose "Found sum $sum"
$sum
}
else {
Write-Verbose "Reducing numbers to $($numbers -join ',')"
Write-Verbose "Setting index to $($index+1)"
Write-Verbose "Get-ReductiveSum $($sum+$numbers[$index])"
Get-ReductiveSum $numbers -index ($index+1) -sum ($sum+$numbers[$index])
Write-Verbose "Get-ReductiveSum $sum"
Get-ReductiveSum $numbers ($index+1) $sum
}
}
Write-Verbose "Using values $($values -join ',')"
Write-Verbose "Verifying unique values"
$values = $values | Get-Unique
if ($values.count -gt 9) {
#this should probably never happen
Write-Warning "You specified $($values.count) values. Only using the first 9"
$Values = $Values[0..8]
}
Write-Verbose "Calculating possible unique sums for $($values -join ',')"
$result = Get-ReductiveSum $Values | Where-Object {$_ -gt 0}
Write-Verbose "Found $($result.count) non-zero sums"
$result | Sort-Object
}
I'll let you try the code to see the Verbose output.
Getting your brain to think the PowerShell way can be tough. That's why these challenges are so helpful. The more you can follow the PowerShell pipeline in your head, the easier it will be for you to use.
It blew my mind. I don’t understand how the Get-ReductiveSum function works.
To be honest, I found a C# example that I turned into PowerShell. Take a look at https://stackoverflow.com/questions/403865/algorithm-to-sum-up-a-list-of-numbers-for-all-combinations.