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.