How to sum multiple items in an object in PowerShell? - powershell

I have:
$report.gettype().name
Object[]
echo $report
Item Average
-- -------
orange 0.294117647058824
orange -0.901960784313726
orange -0.901960784313726
grape 9.91335740072202
grape 0
pear 3.48736462093863
pear -0.0324909747292419
pear -0.0324909747292419
apple 12.1261261261261
apple -0.0045045045045045
I want to create a variable, $total, (such as a hash table) which contains the sum of the 'Average' column for each item, for example,
echo $total
orange -1.5097
grape 9.913
pear 3.423
apple 12.116
Right now I'm thinking of looping through the $report, but it's hell ugly, and I am looking for something more elegant than the following starting point (incomplete):
$tmpPrev = ""
foreach($r in $report){
$tmp = $r.item
$subtotal = 0
if($tmp <> $tmpPrev){
$subtotal += $r.average
}
How could I do this?

Cmdlets Group-Object and Measure-Object help to solve the task in a PowerShell-ish way:
Code:
# Demo input
$report = #(
New-Object psobject -Property #{ Item = 'orange'; Average = 1 }
New-Object psobject -Property #{ Item = 'orange'; Average = 2 }
New-Object psobject -Property #{ Item = 'grape'; Average = 3 }
New-Object psobject -Property #{ Item = 'grape'; Average = 4 }
)
# Process: group by 'Item' then sum 'Average' for each group
# and create output objects on the fly
$report | Group-Object Item | %{
New-Object psobject -Property #{
Item = $_.Name
Sum = ($_.Group | Measure-Object Average -Sum).Sum
}
}
Output:
Sum Item
--- ----
3 orange
7 grape

I've got a more command-line solution.
Given $report
$groupreport = $report | Group-Object -Property item -AsHashTable
is
Name Value
---- -----
grape {#{Item=grape; Average=9.91335740072202}, #{Item=grape; Average=0}}
orange {#{Item=orange; Average=0.294117647058824}, #{Item=orange; Average=-0.901960784313726...
apple {#{Item=apple; Average=12.1261261261261}, #{Item=apple; Average=-0.0045045045045045}}
pear {#{Item=pear; Average=3.48736462093863}, #{Item=pear; Average=-0.0324909747292419}, #...
then
$tab=#{}
$groupreport.keys | % {$tab += #{$_ = ($groupreport[$_] | measure-object -Property average -sum)}}
gives
PS> $tab["grape"]
Count : 2
Average :
Sum : 9,91335740072202
Maximum :
Minimum :
Property : Average
PS> $tab["grape"].sum
9,91335740072202
It seems short and usable.
Summary
$groupreport = $report | Group-Object -Property item -AsHashTable
$tab = #{}
$groupreport.keys | % {$tab += #{$_ = ($groupreport[$_] | measure-object -Property average -sum)}}
$tab.keys | % {write-host $_ `t $tab[$_].sum}

I don't know if you can get rid of looping. What about:
$report | % {$averages = #{}} {
if ($averages[$_.item]) {
$averages[$_.item] += $_.average
}
else {
$averages[$_.item] = $_.average
}
} {$averages}

Related

Count the number of columns in an array with PowerShell

I have the following code:
$array = Get-Process
Now, I want to count the number of columns. I have tried:
$array.Columns.Count
$array.Columns
And all without luck.
What should I do to get the correct number of columns? That should be 8.
<<Added on January, 27th, 2021: >>
I have modified the code to:
$Processes = #(Get-Process | Select-Object -Property Name, Description, ProductVersion, #{Name="Application";Expression={$_.Description + " " + $_.ProductVersion}},#{Name="Executable";Expression={(($_."Path").split("\"))[-1]}} |Where {$_.Executable})
$Array = #()
$Record = [ordered] #{"Name" = "";
"Application" = ""}
ForEach ($ProcesName in $Processes)
{
$Record."Name" = $ProcesName.Name
$Record."Application" = $ProcesName.Application
$objRecord = New-Object PSObject -Property $Record
$Array += $objRecord
}
Clear-Host
Write-Host "Number of columns: $($Array.Column.Count)"
Write-Host "Number rows: $($Array.Count)"
The result is the same: both the column and row are the same. And that is not correct. I want to use the technique in a new script with much more columns.
<<End part that has been added on January, 27th, 2021 >>
Feedback is appreciated.
With kind regards,
TheStingPilot
$array is an array of objects of type System.Diagnostics.Process. There aren't columns as such. The objects have properties which you may choose to display in columns using $array | ft or as a list $array | fl *. You can count the properties like this:
$array | Get-Member -MemberType Property | measure
If you'd like the answer to be 8 then you can do this:
$array | select Handles, NPM, PM, WS, CPU, Id, SI, Name | Get-Member -MemberType NoteProperty | measure

PowerShell Compare-Object to produce change report file

I've looked at numerous examples and have made headway in producing a change report. But, I'm stuck in one area. Here's the scenario...
File 1 CSV file sample data
ID,Name,Location,Gender
1,Peter,USA,Male
2,Paul,UK,Male
3,Mary,PI,Female
File 2 CSV file sample data (No ID column)
Name,Location,Gender
Peter,USA,Female
Paul,UK,Male
Mary,USA,Female
Tom,PI,Female
Barry,CAN,Male
File 2 has changes and additions, i.e. Peter turned female, Mary moved to the US, both Tom and Barry are the new people. Change report output file contain what the changes are. Problem is, I can't figure out how to get the ID for both Peter and Mary from File 1, into my Change Report. ID is always empty Here's my code...(I hope someone can shed some light. Thanks in advance.)
$MyCSVFields = #('Name','Location','Gender')
$CompareResults = Compare-Object $RefObj $DffObj -Property $MyCSVFields -IncludeEqual
$NewOrChangedData = #()
Foreach($Row in $CompareResults)
{
if( $Row.SideIndicator -eq "=>" )
{
$TempObject = [pscustomobject][ordered] #{
ID = $Row.ID
Name = $Row.Name
Location = $Row.Location
Gender = $Row.Gender
#Sanity check "Compare Indicator" = $Row.SideIndicator
}
$NewOrChangedData += $TempObject
}
}
Thanks to Theo for providing an understanding of how to use the Where-Object. Here is the updated code that keeps it simple for beginners and still works for us.
Foreach($Row in $CompareResults)
{
if( $Row.SideIndicator -eq "=>" )
{
$myOrgID = $RefObj | Where-Object Name -eq $Row.Name
$TempObject = [pscustomobject][ordered] #{
ID = $myOrgID.ID
Name = $Row.Name
Location = $Row.Location
Gender = $Row.Gender
#Sanity check "Compare Indicator" = $Row.SideIndicator
}
$NewOrChangedData += $TempObject
}
}
I'm also alway struggling with Compare-Object, so I hope there is a better answer than this:
$RefObj = #'
ID,Name,Location,Gender
1,Peter,USA,Male
2,Paul,UK,Male
3,Mary,PI,Female
'# | ConvertFrom-Csv
$DffObj = #'
Name,Location,Gender
Peter,USA,Female
Paul,UK,Male
Mary,USA,Female
Tom,PI,Female
Barry,CAN,Male
'# | ConvertFrom-Csv
$MyCSVFields = #('Name','Location','Gender')
$CompareResults = Compare-Object $RefObj $DffObj -Property $MyCSVFields -PassThru
$NewOrChangedData = $CompareResults | Where-Object { $_.SideIndicator -eq '=>' } | ForEach-Object {
$name = $_.Name
[PsCustomObject]#{
ID = ($RefObj | Where-Object { $_.Name -eq $name }).ID
Name = $name
Location = $_.Location
Gender = $_.Gender
#Sanity check "Compare Indicator" = $_.SideIndicator
}
}
$NewOrChangedData
Result:
ID Name Location Gender
-- ---- -------- ------
1 Peter USA Female
3 Mary USA Female
Tom PI Female
Barry CAN Male

Conditional criteria in powershell group measure-object?

I have data in this shape:
externalName,day,workingHours,hoursAndMinutes
PRJF,1,11,11:00
PRJF,2,11,11:00
PRJF,3,0,0:00
PRJF,4,0,0:00
CFAW,1,11,11:00
CFAW,2,11,11:00
CFAW,3,11,11:00
CFAW,4,11,11:00
CFAW,5,0,0:00
CFAW,6,0,0:00
and so far code is
$gdata = Import-csv $filepath\$filename | Group-Object -Property Externalname;
$test = #()
$test += foreach($rostername in $gdata) {
$rostername.Group | Select -Unique externalName,
#{Name = 'AllDays';Expression = {(($rostername.Group) | measure -Property day).count}},
}
$test;
What I can't work out is how to do a conditional count of the lines where day is non-zero.
The aim is to produce two lines:
PRJF, 4, 2, 11
CFAW, 6, 4, 11
i.e. Roster name, roster length, days on, average hours worked per day on.
You need a where-object to filter for non zero workinghours
I'd use a [PSCustomObject] to generate a new table
EDIT a bit more efficient with only one Measure-Object
## Q:\Test\2018\08\06\SO_51700660.ps1
$filepath = 'Q:\Test\2018\08\06'
$filename = 'SO_S1700660.csv'
$gdata = Import-Csv (Join-Path $filepath $filename) | Group-Object -Property Externalname
$test = ForEach($Roster in $gdata) {
$WH = ($Roster.Group.Workinghours|Where-Object {$_ -ne 0}|Measure-Object -Ave -Sum)
[PSCustomObject]#{
RosterName = $Roster.Name
RosterLength = $Roster.Count
DaysOn = $WH.count
AvgHours = $WH.Average
TotalHours = $WH.Sum
}
}
$test | Format-Table
Sample output:
> .\SO_51700660.ps1
RosterName RosterLength DaysOn AvgHours TotalHours
---------- ------------ ------ -------- ----------
PRJF 4 2 11 22
CFAW 6 4 11 44

How to use Group-Object on this?

I am trying to get all the accounts from $f which do not match the accounts in $table4 into $accounts. But I need to also check if the occupancy number matches or not.
CSV $f:
Account_no |occupant_code
-----------|------------
12345 | 1
67890 | 2
45678 | 3
DataTable $table4
Account_no |occupant_code
-----------|------------
12345 | 1
67890 | 1
45678 | 3
Current code:
$accounts = Import-Csv $f |
select account_no, occupant_code |
where { $table4.account_no -notcontains $_.account_no }
What this needs to do is to check that occupant_code doesn't match, i.e.:
12345: account and occupant from $f and $table4 match; so it's ignored
67890: account matches $table4, but occupancy_code does not match, so it is added to $accounts.
Current result:
Desired result: 67890
I believe I need to use Group-Object, but I do not know how to use that correctly.
I tried:
Import-Csv $f |
select account_no, occupant_code |
Group-Object account_no |
Where-Object { $_.Group.occupant_code -notcontains $table4.occupant_code }
An alternative to Bill's suggestion would be to fill a hashtable with your reference data ($table4) and look up the occupant_code value for each account from $f, assuming that your account numbers are unique:
$ref = #{}
$table4 | ForEach-Object {
$ref[$_.Account_no] = $_.occupant_code
}
$accounts = Import-Csv $f |
Where-Object { $_.occupant_code -ne $ref[$_.Account_no] } |
Select-Object -Expand Account_no
Compare-Object?
csv1.csv:
Account_no,occupant_code
12345,1
67890,2
45678,3
csv2.csv:
Account_no,occupant_code
12345,1
67890,1
45678,3
PowerShell command:
Compare-Object (Import-Csv .\csv1.csv) (Import-Csv .\csv2.csv) -Property occupant_code -PassThru
Output:
Account_no occupant_code SideIndicator
---------- ------------- -------------
67890 1 =>
67890 2 <=
$f | InnerJoin $table4 {$Left.Account_no -eq $Right.Account_no -and $Left.occupant_code -ne $Right.occupant_code} #{Account_no = {$Left.$_}} | Format-Table
Result:
occupant_code Account_no
------------- ----------
{2, 1} 67890
For details see: In Powershell, what's the best way to join two tables into one?
In addition to all the other answers, you might be able to leverage the IndexOf() method on arrays
$services = get-service
$services.name.IndexOf("xbgm")
240
I am on a tablet right now and don't have a handy way to test it, but something along these lines might work for you:
$table4.account_no.IndexOf($_.account_no)
should fetch the index your account_no lives in for $table 4, so you could jam it all into one ugly pipe:
$accounts = Import-Csv $f | select account_no, occupant_code |
where { ($table4.account_no -notcontains $_.account_no) -or ($table4[$table4.account_no.IndexOf($_.account_no)].occupant_code -ne $_.occupant_code) }
An inner join or a normal loop might just be cleaner though, especially if you want to add some other stuff in. Since someone posted an innerjoin, you could try a loop like:
$accounts = new-object System.Collections.ArrayList
$testSet = $table4.account_no
foreach($myThing in Import-Csv $f)
{
if($myThing.account_no -in $testSet )
{
$i = $testSet.IndexOf($myThing.account_no)
if($table4[$i].occupant_code -eq $myThing.occupant_code) {continue}
}
$accounts.add($myThing)
}
Edit for OP, he mentioned $table4 is a data.table
There is probably a much better way to do this, as I haven't used data.table before, but this seems to work fine:
$table = New-Object system.Data.DataTable
$col1 = New-Object system.Data.DataColumn Account_no,([string])
$col2 = New-Object system.Data.DataColumn occupant_code,([int])
$table.columns.add($col1)
$table.columns.add($col2)
$row = $table.NewRow()
$row.Account_no = "12345"
$row.occupant_code = 1
$table.Rows.Add($row)
$row = $table.NewRow()
$row.Account_no = "67890"
$row.occupant_code = 1
$table.Rows.Add($row)
$row = $table.NewRow()
$row.Account_no = "45678"
$row.occupant_code = 3
$table.Rows.Add($row)
$testList = #()
$testlist += [pscustomobject]#{Account_no = "12345"; occupant_code = 1}
$testlist += [pscustomobject]#{Account_no = "67890"; occupant_code = 2}
$testlist += [pscustomobject]#{Account_no = "45678"; occupant_code = 3}
$accounts = new-object System.Collections.ArrayList
$testSet = $table.account_no
foreach($myThing in $testList)
{
if($myThing.account_no -in $testSet )
{
$i = $testSet.IndexOf($myThing.account_no)
if($table.Rows[$i].occupant_code -eq $myThing.occupant_code) {continue}
}
$accounts.add($myThing) | out-null
}
$accounts

Seeking balanced combination of fast, terse, and legible code to add up values from an array of objects

Given the following array of objects:
Email Domain Tally
----- ----- -----
email1#domainA.com domainA.com 4
email1#domainB.com domainB.com 1
email2#domainC.com domainC.com 6
email4#domainA.com domainA.com 1
I'd like to "group by" Domain and add up Tally as I go. The end result would like this:
Domain Tally
------ -----
domainA.com 5
domainB.com 1
domainC.com 6
I have something that works but I feel like it's overly complicated.
$AllTheAddresses = Get-AllTheAddresses
$DomainTally = #()
foreach ($Addy in $AllTheAddresses)
{
if ($DomainTally | Where-Object {$_.RecipientDomain -eq $Addy.RecipientDomain})
{
$DomainTally |
Where-Object {$_.RecipientDomain -eq $Addy.RecipientDomain} |
ForEach-Object {$_.Tally += $Addy.Tally }
}
else
{
$props = #{
RecipientDomain = $Addy.RecipientDomain
Tally = $Addy.Tally
}
$DomainTally += New-Object -TypeName PSObject -Property $props
}
}
In my example, I'm creating the addresses as hashtables, but PowerShell will let you refer to the keys by .Property similar to an object.
If you're truly just summing by the Domain, then it seems like you don't need anything more complicated than a HashTable to create your running total.
The basic summation:
$Tally = #{}
$AllTheAddresses | ForEach-Object {
$Tally[$_.Domain] += $_.Tally
}
Using this sample data...
$AllTheAddresses = #(
#{ Email = "email1#domainA.com"; Domain = "domainA.com"; Tally = 4 };
#{ Email = "email1#domainB.com"; Domain = "domainB.com"; Tally = 1 };
#{ Email = "email1#domainC.com"; Domain = "domainC.com"; Tally = 6 };
#{ Email = "email1#domainA.com"; Domain = "domainA.com"; Tally = 1 }
)
And you get this output:
PS> $tally
Name Value
---- -----
domainC.com 6
domainB.com 1
domainA.com 5
Here is a "PowerShellic" version, notice the piping and flow of the data.
You could of course write this as a one liner (I did originally before I posted the answer here). The 'better' part of this is using the Group-Object and Measure-Object cmdlets. Notice there are no conditionals, again because the example uses the pipeline.
$AllTheAddresses |
Group-Object -Property Domain |
ForEach-Object {
$_ |
Tee-Object -Variable Domain |
Select-Object -Expand Group |
Measure-Object -Sum Tally |
Select-Object -Expand Sum |
ForEach-Object {
New-Object -TypeName PSObject -Property #{
'Domain' = $Domain.Name
'Tally' = $_
}
} |
Select-Object Domain, Tally
}
A more terse version
$AllTheAddresses |
Group Domain |
% {
$_ |
Tee-Object -Variable Domain |
Select -Expand Group |
Measure -Sum Tally |
Select -Expand Sum |
% {
New-Object PSObject -Property #{
'Domain' = $Domain.Name
'Tally' = $_
}
} |
Select Domain, Tally
}
Group-Object is definitely the way to go.
In the interest of terseness:
Get-AllTheAddresses |Group-Object Domain |Select-Object #{N='Domain';E={$_.Name}},#{N='Tally';E={($_.Group.Tally |Measure-Object).Sum}}