PowerShell foreach() multidimensional array based on first element - arrays

I have a fairly basic multidimensional array which looks something like this:
2017,123
2017,25
2018,5
2018,60
2017,11
I wish to run a ForEach() loop or similar function to total the numbers in the second element based on the year indicated in the first so that I end up with an output like this:
2017,159
2018,65
How do I best accomplish this?

The following solution is concise, but not fast:
# input array
$arr =
(2017,123),
(2017,25),
(2018,5),
(2018,60),
(2017,11)
# Group the sub-arrays by their 1st element and sum all 2nd elements
# in each resulting group.
$arr | Group-Object -Property { $_[0] } | ForEach-Object {
, ($_.Name, (($_.Group | ForEach-Object { $_[1] } | Measure-Object -Sum).Sum))
}

Assuming your array looks like "$array" this will give you what you need:
$2017total = 0
$2018total = 0
$array = "2017,123",
"2017,25",
"2018,5",
"2018,60",
"2017,11" | % {
if ($_ -match '2017') {
$2017 = ($_ -split ',')[1]
$2017total += $2017
}
else {
$2018 = ($_ -split ',')[1]
$2018total += $2018
}
}
Write-Host "2017,$2017total"
Write-Host "2018,$2018total"

Related

PowerShell array, loop first row

I have a problem with array as following with only one line:
$list = #()
$list = (("ResourceGroup","Vm1"))
$list | ForEach-Object -Parallel {
write-output $_[0] $_[1]
}
If I loop that array with one line, PowerShell prints the first 2 letter of each word. If I put 2 or more row like following:
$list = #()
$list = (("ResourceGroup","Vm1"),`
("ResourceGroup","Vm2")
)
PowerShell print correctly the values inside.
There is a way to print correctly the value of an array with only one line?
("ResourceGroup","Vm1") is interpreted as one array with two string elements, where as (("ResourceGroup","Vm1"), ("ResourceGroup","Vm2")) is interpreted as one array with 2 array elements, can be also called a jagged array. If you want to ensure that the first example is treated the same as the second example, you can use the comma operator ,:
$list = , ("ResourceGroup","Vm1")
$list | ForEach-Object -Parallel {
Write-Output $_[0] $_[1]
}
To put it in perspective:
$list = ("ResourceGroup","Vm1")
$list[0].GetType() # => String
$list = , ("ResourceGroup","Vm1")
$list[0].GetType() # => Object[]
Write-Output with the -NoEnumerate switch combined with the Array subexpression operator #( ) can be another, more verbose, alternative:
$list = #(Write-Output "ResourceGroup", "Vm1" -NoEnumerate)
$list | ForEach-Object -Parallel {
Write-Output $_[0] $_[1]
}

Query PSCustomObject Array for row with largest value

I'm trying to find the row with an attribute that is larger than the other row's attributes. Example:
$Array
Name Value
---- ----
test1 105
test2 101
test3 512 <--- Selects this row as it is the largest value
Here is my attempt to '1 line' this but It doesn't work.
$Array | % { If($_.value -gt $Array[0..($Array.Count)].value){write-host "$_.name is the largest row"}}
Currently it outputs nothing.
Desired Output:
"test1 is the largest row"
I'm having trouble visualizing how to do this efficiently with out some serious spaghetti code.
You could take advantage of Sort-Object to rank them by the property "Value" like this
$array = #(
[PSCustomObject]#{Name='test1';Value=105}
[PSCustomObject]#{Name='test2';Value=101}
[PSCustomObject]#{Name='test3';Value=512}
)
$array | Sort-Object -Property value -Descending | Select-Object -First 1
Output
Name Value
---- -----
test3 512
To incorporate your write host you can just run the one you select through a foreach.
$array | Sort-Object -Property value -Descending |
Select-Object -First 1 | Foreach-Object {Write-host $_.name,"has the highest value"}
test3 has the highest value
Or capture to a variable
$Largest = $array | Sort-Object -Property value -Descending | Select-Object -First 1
Write-host $Largest.name,"has the highest value"
test3 has the highest value
PowerShell has many built in features to make tasks like this easier.
If this is really an array of PSCustomObjects you can do something like:
$Array =
#(
[PSCustomObject]#{ Name = 'test1'; Value = 105 }
[PSCustomObject]#{ Name = 'test2'; Value = 101 }
[PSCustomObject]#{ Name = 'test3'; Value = 512 }
)
$Largest = ($Array | Sort-Object Value)[-1].Name
Write-host $Largest,"has the highest value"
This will sort your array according to the Value property. Then reference the last element using the [-1] syntax, then return the name property of that object.
Or if you're a purist you can assign the variable like:
$Largest = $Array | Sort-Object Value | Select-Object -Last 1 -ExpandProperty Name
If you want the whole object just remove .Name & -ExpandProperty Name respectively.
Update:
As noted PowerShell has some great tools to help with common tasks like sorting & selecting data. However, that doesn't mean there's never a need for looping constructs. So, I wanted to make a couple of points about the OP's own answer.
First, if you do need to reference array elements by index use a traditional For loop, which might look something like:
For( $i = 0; $i -lt $Array.Count; ++$i )
{
If( $array[$i].Value -gt $LargestValue )
{
$LargestName = $array[$i].Name
$LargestValue = $array[$i].Value
}
}
$i is commonly used as an iteration variable, and within the script block is used as the array index.
Second, even the traditional loop is unnecessary in this case. You can stick with the ForEach loop and track the largest value as and when it's encountered. That might look something like:
ForEach( $Row in $array )
{
If( $Row.Value -gt $LargestValue )
{
$LargestName = $Row.Name
$LargestValue = $Row.Value
}
}
Strictly speaking you don't need to assign the variables beforehand, though it may be a good practice to precede either of these with:
$LargestName = ""
$LargestValue = 0
In these examples you'd have to follow with a slightly modified Write-Host command
Write-host $LargestName,"has the highest value"
Note: Borrowed some of the test code from Doug Maurer's Fine Answer. Considering our answers were similar, this was just to make my examples more clear to the question and easier to test.
Figured it out, hopefully this isn't awful:
$Count = 1
$CurrentLargest = 0
Foreach($Row in $Array) {
# Compare This iteration vs the next to find the largest
If($Row.value -gt $Array.Value[$Count]){$CurrentLargest = $Row}
Else {$CurrentLargest = $Array[$Count]}
# Replace the existing largest value with the new one if it is larger than it.
If($CurrentLargest.Value -gt $Largest.Value){ $Largest = $CurrentLargest }
$Count += 1
}
Write-host $Largest.name,"has the highest value"
Edit: its awful, look at the other answers for a better way.

Powershell how to count all elements in a multidimensional array

I've been trying to figure out how to count all elements in an multidimensional array. But .Count only returns the first dimension.
after i gave up to find a proper solution i just created this loop to move all elements to the first dimension and count them. but this is really only a hack.
$mdarr = #((0,1,2,3,4),(5,6,7,8,9),(10,11,12,13,14))
$filecount = New-Object System.Collections.ArrayList
for($i = 0; $i -lt $mdarr.Length; ++$i) {
$filecount += $mdarr[$i]
}
$filecount.Count
How would this be done properly without processing the array first?
In the loop you are adding the elements of $mdarr[$i]. You later count the elements of the merge result. Instead of the adding to an ArrayList you could keep a count:
$xs = #((0,1,2,3,4),(5,6,7,8,9),(10,11,12,13,14))
$sum = 0;
foreach ($x in $xs) { $sum += $x.Count }
$sum // 15
# alternatively
$xs | % { $sum += $_.Count }
# or
($xs | % { $_.Count } | Measure-Object -Sum).Sum
# or
$xs | % { $_.Count } | Measure-Object -Sum | select -Expand Sum
one line code: you can flatten the multidimensional array into a anonymous array, and count the anonymous array
$xs = #((0,1,2,3,4),(5,6,7,8,9),(10,11,12,13,14))
#($xs | ForEach-Object {$_}).count #result 15
or multiline that is more readable:
$xs = #((0,1,2,3,4),(5,6,7,8,9),(10,11,12,13,14))
$xs_flatten = #($xs | ForEach-Object {$_})
$xs_flatten_count = $xs_flatten.count
echo $xs_flatten_count #result 15
put a dimension identifier index in front of .count
e.g $xs[0].count
this way, instead of returning the count of dimensions, it returns the number of rows for a given dimension

Display all values in PSCustomObject array [duplicate]

Is it possible to display the results of a PowerShell Compare-Object in two columns showing the differences of reference vs difference objects?
For example using my current cmdline:
Compare-Object $Base $Test
Gives:
InputObject SideIndicator
987654 =>
555555 <=
123456 <=
In reality the list is rather long. For easier data reading is it possible to format the data like so:
Base Test
555555 987654
123456
So each column shows which elements exist in that object vs the other.
For bonus points it would be fantastic to have a count in the column header like so:
Base(2) Test(1)
555555 987654
123456
Possible? Sure. Feasible? Not so much. PowerShell wasn't really built for creating this kind of tabular output. What you can do is collect the differences in a hashtable as nested arrays by input file:
$ht = #{}
Compare-Object $Base $Test | ForEach-Object {
$value = $_.InputObject
switch ($_.SideIndicator) {
'=>' { $ht['Test'] += #($value) }
'<=' { $ht['Base'] += #($value) }
}
}
then transpose the hashtable:
$cnt = $ht.Values |
ForEach-Object { $_.Count } |
Sort-Object |
Select-Object -Last 1
$keys = $ht.Keys | Sort-Object
0..($cnt-1) | ForEach-Object {
$props = [ordered]#{}
foreach ($key in $keys) {
$props[$key] = $ht[$key][$_]
}
New-Object -Type PSObject -Property $props
} | Format-Table -AutoSize
To include the item count in the header name change $props[$key] to $props["$key($($ht[$key].Count))"].

Reverse elements via pipeline

Is there a function that reverses elements passed via pipeline?
E.g.:
PS C:\> 10, 20, 30 | Reverse
30
20
10
You can cast $input within the function directly to a array, and then reverse that:
function reverse
{
$arr = #($input)
[array]::reverse($arr)
$arr
}
10, 20, 30 | Sort-Object -Descending {(++$script:i)}
Here's one approach:
function Reverse ()
{
$arr = $input | ForEach-Object { $_ }
[array]::Reverse($arr)
return $arr
}
Using $input works for pipe, but not for parameter. Try this:
function Get-ReverseArray {
Param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeLine = $true)]
$Array
)
begin{
$build = #()
}
process{
$build += #($Array)
}
end{
[array]::reverse($build)
$build
}
}
#set alias to use non-standard verb, not required.. just looks nicer
New-Alias -Name Reverse-Array -Value Get-ReverseArray
Test:
$a = "quick","brown","fox"
#--- these all work
$a | Reverse-Array
Reverse-Array -Array $a
Reverse-Array $a
#--- Output for each
fox
brown
quick
Here's a remarkably compact solution:
function Reverse
{
[System.Collections.Stack]::new(#($input))
}
I realize this doesn't use a pipe, but I found this easier if you just wanted to do this inline, there is a simple way. Just put your values into an array and call the existing Reverse function like this:
$list = 30,20,10
[array]::Reverse($list)
# print the output.
$list
Put that in a PowerShell ISE window and run it the output will be 10, 20, 30.
Again, this is if you just want to do it inline. There is a function that will work for you.
$array = 10,20,30; (($array.Length - 1)..0) | %{ $array[$_] }
You can use Linq.Enumerable.Reverse, but it's hard work. Either of the following work:
,(10, 20, 30) | % { [Linq.Enumerable]::Reverse([int[]]$_) }
,([int[]]10, 20, 30) | % { [Linq.Enumerable]::Reverse($_) }
The two challenges are putting the whole array into the pipeline rather than one element at a time (solved by making it an array of arrays with the initial comma) and hitting the type signature of Reverse (solved by the [int[]]).
Try:
10, 20, 30 | Sort-Object -Descending

Resources