A few weeks ago, an Iron Scripter PowerShell scripting challenge was posted. As with all of these challenges, the process is more important than the end result. How you figure out a solution is how you develop as a PowerShell professional. A few people have already shared their work. Today, I thought I'd share mine.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Beginner
The beginner challenge was to get the sum of even numbers between 1 and 100. You should be able to come up with at least 3 different techniques.
For Loop
My first solution is to use a For loop.
$t = 0
for ($i = 2; $i -le 100; $i += 2) {
$t += $i
}
$t
The variable $t is set to 0. This will be the total value. The syntax of the For loop says, "Start with $i and a value of 2 and keep looping while $i is less than or equal to 100. Every time you loop, increase the value of $i by 2." The += operator is a shortcut way of saying $i = $i+2. This should get all even numbers up to and including 100. I'm using the -le operator and not the -lt operator. Each time through the loop, the code inside the { }, I'm incrementing $t by the value of $i. In the end, I get a value of 2550 for $t.
Modulo Operator
The next technique is to use the Modulo operator - %.
1..100 | Where-Object {-Not($_%2)} | Measure-Object -sum
The first part of the pipelined expression is using the Range operator to get all numbers between 1 and 100. Each number is piped to Where-Object which uses the Modulo operator. I'm dividing each number by 2. If there is a remainder, for example, 3/2 is 1.5, the result is 1. Otherwise, the operator produces a result of 0. 1 and 0 can also be interpreted as boolean values. 1 is $True and 0 is $False. In this case, all the numbers divided by 2 with no remainder will produce a value of 0. But I need Where-Object to pass objects when the expression is True so I use the -Not operator to reverse the value. Thus False becomes True and the number is passed to Measure-Object where I can get the sum. And yes, there are other ways you could have written the Where-Object expression.
ForEach Loop
My third idea was to use a ForEach loop.
$t = 0
foreach ($n in (1..100)) {
if ($n/2 -is [int]) {
$t += $n
}
}
$t
The code says, "Foreach thing in the collection, do something." The collection is the range 1..100. The "thing" can be called anything you want. In my code, this is $n. For each value, I'm using an IF statement to test if $n/2 is an object of type [int]. A value like 1.5 is technically a [double]. Assuming the value passes the test, I increment $t by that value.
Bonus
The "right" solution depends on the rest of your code and what makes the most sense. That's why there are different techniques for you to learn. If I just wanted a simple one-line solution, I could do something like this:
(1..100 | where-object {$_/2 -is [int]} | measure-object -sum).sum
Instead of getting the measurement object, I'm getting just the sum property. The ( ) tell PowerShell, "run this code and hold on to the object(s)". This is a one-line version of this:
$m = 1..100 | where-object {$_/2 -is [int]} | measure-object -sum
$m.sum
With the (), $m is implicitly being defined.
Intermediate
The next level challenge was to write a function that would get the sum of every X value between 1 and a user-specified maximum. We were also asked to get the average and ideally include all the numbers that were used in the calculation.
For development purposes, I always start with simple code. I tested getting the sum of every 6th number between 1 and 100.
$total = 0
$max = 100
$int = 6
$count = 0
$Capture = @()
for ($i = $int; $i -le $max; $i += $int) {
$count++
$Capture += $i
$total += $i
}
[pscustomobject]@{
Start = 1
End = $Max
Interval = $int
Sum = $total
Average = $total/$count
Values = $Capture
}
You'll see that I'm using similar operators and techniques. The new step is that I am creating a custom object on the fly. The hashtable keys become the property names.
Start : 1
End : 100
Interval : 6
Sum : 816
Average : 51
Values : {6, 12, 18, 24...}
With this, I was able to create this function.
Function Get-NFactorial {
[CmdletBinding()]
Param (
[Parameter(Position = 0)]
[int32]$Start = 1,
[Parameter(Mandatory, Position = 1)]
[int32]$Maximum,
[Parameter(Mandatory)]
[ValidateRange(1, 10)]
[ArgumentCompleter({1..10})]
[int32]$Interval
)
Begin {
Write-Verbose "Starting $($MyInvocation.Mycommand)"
Write-Verbose "Getting NFactorial values between $start and $Maximum"
Write-Verbose "Using an interval of $Interval"
#initialize some variables
$count = 0
$Capture = @()
$Total = 0
} #begin
Process {
Write-Verbose "Looping through the range of numbers"
for ($i = $Interval; $i -le $Maximum; $i += $Interval) {
$count++
$Capture += $i
$Total += $i
}
Write-Verbose "Writing result to the pipeline"
[pscustomobject]@{
PSTypeName = "nFactorial"
Start = $Start
End = $Maximum
Interval = $Interval
Sum = $Total
Average = $total/$count
Values = $Capture
ValuesCount = $Capture.Count
}
} #process
End {
Write-Verbose "Ending $($MyInvocation.Mycommand)"
} #end
} #close function
For the Interval parameter, I'm doing something you may not have seen before. I'm limiting the user to using an interval value between 1 and 10. That's the ValidateRange attribute you may have seen before. The other element is an ArgumentCompleter. When someone runs the function, I want them to be able to tab-complete the value for -Interval. I've noticed that when you use ValidateSet, tab completion works. But not with ValidateRange. (Yes, I could have used ValidateSet, but then I wouldn't have anything to teach you!). The ArgumentCompleter uses the results of the code inside the {} as autocomplete values.
Here's the function in action.
PS C>\> Get-Nfactorial -Start 1 -Maximum 100 -Interval 2
Start : 1
End : 100
Interval : 2
Sum : 2550
Average : 51
Values : {2, 4, 6, 8...}
ValuesCount : 50
Let's take this one more step.
Formatting Results
The default output is a list and maybe I don't really need to see all of these properties by default. If you noticed, in the custom object hashtable I defined a property called PSTypeName. In order to do custom formatting, your object needs a unique type name. Mine is called nFactorial.
Formatting is going to require a ps1xml file. But don't freak out. Install the PSScriptTools module from the PowerShell Gallery and use New-PSFormatXML.
Get-Nfactorial -Start 1 -Maximum 100 -Interval 2 | New-PSFormatXML -Path .\nfactorial.format.ps1xml -Properties Start,End,Interval,Sum,Average -FormatType Table
You can edit the file and adjust it as needed. Here's my version.
<?xml version="1.0" encoding="UTF-8"?>
<!--
format type data generated 05/19/2020 10:06:19 by BOVINE320\Jeff
-->
<Configuration>
<ViewDefinitions>
<View>
<!--Created 05/19/2020 10:06:19 by BOVINE320\Jeff-->
<Name>default</Name>
<ViewSelectedBy>
<TypeName>nFactorial</TypeName>
</ViewSelectedBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.
<AutoSize />
-->
<TableHeaders>
<TableColumnHeader>
<Label>Start</Label>
<Width>5</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>End</Label>
<Width>6</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Interval</Label>
<Width>8</Width>
<Alignment>center</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Sum</Label>
<Width>9</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Average</Label>
<Width>10</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Start</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>End</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Interval</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>"{0:n0}" -f $_.Sum</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Average</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
To use, you need to update PowerShell.
Update-FormatData .\nfactorial.format.ps1xml
With this in place, I now get nicely formatted output.

Remember, this is only my new default if I don't tell PowerShell to do anything else. I can still run the function and pipe to Select-Object or Format-List.
This silly function is clearly far from practical, but I can use these techniques and patterns in future projects. I hope you got something out of this and I encourage you to tackle the Iron Scripter challenges as they come along.

2 thoughts on “Solving the PowerShell Counting Challenge”
Comments are closed.