Sort a nested hash table by value - powershell

I have a hash array as such:
$weeklyStats[$RecipientName][$weekNr][$total]
Which is created in a loop as such:
$weeklyStats = #{}
$weekNr = get-date -UFormat %V
ForEach ($RecipientName in $MailTraffic.keys)
{
$weeklyStats[$RecipientName] = #{}
$weeklyStats[$RecipientName][$weekNr] = #{}
$weeklyStats[$RecipientName][$weekNr]['Total'] = 0
$weeklyStats[$RecipientName][$weekNr]['Sent'] = 0
$weeklyStats[$RecipientName][$weekNr]['Received'] = 0
foreach($item in $MailTraffic[$RecipientName].keys)
{
weeklyStats[$RecipientName][$weekNr]['Total'] =+ 1
if $MailTraffic[$RecipientName]['transaction'] == "Sent"
{
$weeklyStats[$RecipientName][$weekNr]['Sent'] =+ 1
}
else
{
$weeklyStats[$RecipientName][$weekNr]['Received'] =+ 1
}
}
}
I don't know how to 'dump' a variable in Powershell but here is the contents in json:
{
"mike": {
"11": {
"Total": 411,
"Sent": 21,
"Received":390,
}
},
"peter": {
"11": {
"Total": 751,
"Sent": 51,
"Received":700,
}
},
"frank": {
"11": {
"Total": 620,
"Sent": 20,
"Received":600,
}
},
}
I want to print out the keys and values in descending order of the $total.
I can only find examples how to do it if the hash table is only one level deep.
The intended output would be:
Name Total Received Sent
----- ----- ----- -----
peter 751 700 51
frank 620 600 20
mike 411 390 21

Sort by referencing the Keys property of the inner hashtable, then assign to a new [ordered] dictionary:
$sorted = [ordered]#{}
$stats.GetEnumerator() |Sort-Object {
# Sort by first key from each inner hashtable
$_.Value.Keys |Select -First 1
} -Descending |ForEach-Object {
# re-assign to our ordered dictionary
$sorted[$_.Key] = $_.Value
}
$sorted now contains your new sorted dictionary

Most PowerShell cmdlets are intended to handle (stream!) [PSObject] type (which includes a [PScustomerObject] type) lists for input and output.
(To understand the difference see e.g. Difference between PSObject, Hashtable, and PSCustomObject).
Nested hash tables are difficult to maintain and handle in PowerShell (see also: Powershell Multidimensional Arrays) because PowerShell is optimized for streaming which is rather difficult with cascaded objects, therefore I recommend you convert you nested hashtable in a (rather flat) [PScustomerObject] list, something like:
$PSStats =
ForEach ($name in $Stats.Keys) {
ForEach ($weekNr in $_.Keys) {
ForEach ($total in $_.Keys) {
[pscustomobject]#{name = $name; weekNr = $weekNr; total = $total}
}
}
}
Once you have converted it into PSCustomObject list, you can easily sort it and display the results:
$PSStats | Sort-Object Total

I would just create a custom object from your hash tables and then sort on the Title property:
# Creation of $mailtraffic
$mailtraffic = #{'Mike' = #{'Total' = 411; 'Sent' = 21; 'Received' = 390};'Peter' = #{'Total' = 751; 'Sent' = 51; 'Received' = 700};'Frank' = #{'Total' = 620; 'Sent' = 20; 'Received' = 600}}
# Sorting Code
$mailtraffic.GetEnumerator() |
Select #{n='Name';e={$_.Key}},#{n='Total';e={$_.Value.Total}},#{n='Received';e={$_.Value.Received}},#{n='Sent';e={$_.Value.Sent}} |
Sort Total -Descending

Related

Powershell hashtable with multiple values and one key

Im looking for a data structure/cmdlet that will allow me to add multiple values to a single key in Powershell.
My data would ideally look like this:
KEY-------------------------- VALUES
HOSTNAME1-------------DATABASE1,DATABASE2,DATABASE3
HOSTNAME2-------------DATABASE1,DATABASE2
etc...
I thought a hashtable would do the trick, but I'm unable to do the following:
$servObjects = #{}
$servObjects.Add("server1", #())
$servObjects.get_item("server1") += "database1"
This yields an empty array when I try:
$servObjects.get_item("server1")
I have also tried to do the following, hoping that powershell would understand what I want:
$servObjects2 = #{}
$servObjects2.add($servername, $databasename)
This will unfortunately yield a duplicate key exception
Thanks for any and all input
You basically want a hash table with values that are arrays. You don't have to use $hashtable.get_item or .add
$myHashTable = #{} # creates hash table
$myHashTable.Entry1 = #() #adds an array
$myHashTable.Entry1 += "element1"
$myHashTable.Entry1 += "element2"
This results in the following output:
$myHashTable
Name Value
---- -----
Entry1 {element1, element2}
$myHashTable.Entry1
element1
element2
If you have your data in an array you can group the array and convert to a hash table:
$ary = #()
$ary = $ary + [PSCustomObject]#{RowNumber = 1; EmployeeId = 1; Value = 1 }
$ary = $ary + [PSCustomObject]#{RowNumber = 2; EmployeeId = 1; Value = 2 }
$ary = $ary + [PSCustomObject]#{RowNumber = 3; EmployeeId = 2; Value = 3 }
$ary = $ary + [PSCustomObject]#{RowNumber = 4; EmployeeId = 2; Value = 4 }
$ary = $ary + [PSCustomObject]#{RowNumber = 5; EmployeeId = 3; Value = 5 }
$ht = $ary | Group-Object -Property EmployeeId -AsHashTable
$ht is then:
Name Value
---- -----
3 {#{RowNumber=5; EmployeeId=3; Value=5}}
2 {#{RowNumber=3; EmployeeId=2; Value=3}, #{RowNumber=4; EmployeeId=2; Value=4}}
1 {#{RowNumber=1; EmployeeId=1; Value=1}, #{RowNumber=2; EmployeeId=1; Value=2}}
In your original example, instead of writing
$servObjects.get_item("server1") += "database1"
you had written
$servObjects.server1 += "database1"
it would have worked.
I'm very new to PowerShell, but I prefer to use
$servObjects.Add("key",#())
over
$servObjects.key = #())
because the .Add will throw a duplicate exception if the key is already present in the hashtable, whereas the assignment will replace an existing entry with a new one. For my purposes, I have found that the implicit replacement is (more often than not) an error, either in my logic, or an anomaly in the input data that needs to be handled.
If you know the value at creation time, it would be clearer this way :
[hashtable]$hash = #{
HOSTNAME1 = #(DATABASE1, DATABASE2, DATABASE3);
HOSTNAME2 = #(DATABASE1, DATABASE2);
}
Which will get you the following :
Name Value
---- -----
HOSTNAME2 {DATABASE1, DATABASE2}
HOSTNAME1 {DATABASE1, DATABASE2, DATABASE3}

Masking fields duplications

I am trying to mask fields in a string as seen below. It is working to an extent, half way really. At some stage after the $addresspostcode the replacement characters aren't replacing for the correct positions. Would anyone have an idea of a fix?
The adressee0 line is from the output file
ADDRESSEE0|XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX|YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY|YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY|YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY|YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY|ZZZZZZZZ|Sir or MadamZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ |A1|OM|Mr Patrick MurphyZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX|45 CregtownZZZZZZZZZZZZZZZZ |EastRoad RoadZZZZZZZZZZZZZZZZ |TownnamersZZZZZZZZZZZZZZZZ |CityAB 16ZZZZZZZZZZZZZZZZ |ZZZZZZZZ| |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX|ZZZZZZZZZZZZZZZZ |Sir or MadamZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ |IA|3319041| | |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZZZZZZZZ
ForEach-Object {
$addresseeName = $_.Substring(11,50)
$addresseeName2 = $_.Substring(62,50)
$addresseeLine1 = $_.Substring(113,30)
$addresseeLine2 = $_.Substring(144,30)
$addresseeLine3 = $_.Substring(175,30)
$addresseeLine4 = $_.Substring(206,30)
$addresseePostCode = $_.Substring(237,8)
$referenceAddressName1 = $_.Substring(303,50)
$referenceAddressName2 = $_.Substring(354,50)
$referenceAddresseeLine1 = $_.Substring(405,30)
$referenceAddresseeLine2 = $_.Substring(436,30)
$referenceAddresseeLine3 = $_.Substring(467,30)
$referenceAddresseeLine4 = $_.Substring(498,30)
$mask50 = 'X' * 50
$mask30 = 'Y' * 30
$mask08 = 'Z' * 8
# IF statement, if the string is at position 0-10, and begins with 'ADDRESSEE0'
# then run replace statement
if ($_.Substring(0,10) -eq 'ADDRESSEE0') {
$_.Replace($addresseeName, $mask50).Replace($addresseeName2, $mask50).Replace($addresseeLine1, $mask30).Replace($addresseeLine2, $mask30).Replace($addresseeLine3, $mask30).Replace($addresseeLine4, $mask30).Replace($addresseePostCode, $mask08).Replace($referenceAddressName1, $mask50).Replace($referenceAddressName2, $mask50).Replace($referenceAddresseeLine1, $mask30).Replace($referenceAddresseeLine2, $mask30).Replace($referenceAddresseeLine3, $mask30).Replace($referenceAddresseeLine4, $mask30)
The problem with your code is that it does the replace based on the contents of the string (rather than the position). This means if the same text exists elsewhere in the string, it replaces that text also, breaking the later replaces.
I suggest you do this instead:
$mask50 = 'X' * 50
$mask30 = 'Y' * 30
$mask08 = 'Z' * 8
$SomeInput | ForEach-Object {
if ($_.Substring(0,10) -eq 'ADDRESSEE0')
{
$SplitString = $_.Split('|')
1..2 | ForEach-Object { $SplitString[$_] = $mask50 }
3..6 | ForEach-Object { $SplitString[$_] = $mask30 }
$SplitString[7] = $mask08
8..9 | ForEach-Object { $SplitString[$_] = $mask50 }
10..13 | ForEach-Object { $SplitString[$_] = $mask30 }
$SplitString -Join '|'
}
}
This splits the string based on the | character and then does individual replaces for each (we use the .. array notation to make this a little more efficient).
Then we join the string again at the end with the | character.
Assuming ADDRESSEE0|... is the input string, why not split the data first? This provides manageable chunks instead of one giant string with truckload of method chaining. Like so,
# Get input data
$raw = `'ADDRESSEE0|ADDRESSEE1|ADDRESSEELine0|ADDRESSEELine1|...'`
# Split the string by each pipe | char. This uses regex syntax, so escape \ is needed
$lines = $raw -split '\|'
# Assign splitted elements into more readable variables
$addresseeName = $lines[0]
$addresseeName2 = $lines[1]
$addresseeLine1 = $lines[2]
...
# mask the data whatever way floats your boat
$addresseeName = $addresseeName.substring(0,9) + $mask08

PowerShell HashTable - self referencing during initialization

I have a theoretical problem - how to reference a hash table during its initialization, for example, to compute a member based other already stated members.
Remove-Variable myHashTable -ErrorAction Ignore
$myHashTable =
#{
One = 1
Two= 2
Three = ??? # following expressions do not work
# $This.One + $This.Two or
# $_.One + $_.Two
# $myHashTable.One + $myHashTable.Two
# ????
}
$myHashTable.Three -eq 3 # make this $true
Any ideas how to do it? Is it actually possible?
Edit:
This was my solution:
$myHashTable =
#{
One = 1
Two= 2
}
$myHashTable.Three = $myHashTable.One + $myHashTable.Two
This won't be possible using the object initializer syntax I'm afraid. While it is possible to use variables, you'll have to compute the values before creating the object.
I cannot recommend this, but you can iterate the initializer twice or more:
(0..1) | %{
$a = #{
One = 1
Two = $a.One + 1
}
}
(0..2) | %{
$b = #{
One = 1
Two = $b.One + 1
Three = $b.Two + 1
}
}
Make sure all calculations are idempotent, i.e. do not depend on a number of iterations.
You can also recur to this...
sometimes when the hashtable is very long
and can be defined only in 2 or three recurrences...
works fine:
$AAA = #{
DAT = "C:\MyFolderOfDats"
EXE = "C:\MyFolderOfExes"
}
$AAA += #{
Data = $AAA.DAT + "\#Links"
Scripts = $AAA.EXE + "\#Scripts"
ScriptsX = $AAA.EXE + "\#ScriptsX"
}
Note in the second part we are just adding ( += ) more items to the first part... but now... we can refer the items in first part
of the hashtable

How to create a script that would calculate the difference between samples in a list?

I am trying to create a script that would calculate the difference between samples in a list.
If we take this example:
- result1 = 33
- result2 = 45
- result3 = 66
- result4 = 47
- result"n" = 50
The calculus should start at the second result from the list and descend until the last result, and then sum up those results:
result2 - result1 = 12,
result3 - result2 = 21,
result4 - result3 = 19,
result"n" - result4= 3
sum = 12 + 21 + 19 + 3 = 55
I am new at scripting, and so far i only came up with this solution:
$numbers
$1=[math]::abs($numbers[0]-$numbers[1])
$2=[math]::abs($numbers[1]-$numbers[2])
$3=[math]::abs($numbers[2]-$numbers[3])
$4=[math]::abs($numbers[3]-$numbers[4])
write-host "the results = $1, $2, $3, $4"
$sum = $1 + $2 + $3 + $4
The problem is that the list is dynamic and changes in length, one time there are 10 results and one time 20 for example.
I found a similar question here, but i don't know how to implement the solution to my case, as that is too complicated for me.
What you need is a For loop. It is structured as such:
For(<initial declaration, usually a start point like $i = 0>; <Condition to stop when false>;<Action to perform on each iteration to progress loop>){
Code to perform on each loop
}
For you we would do something like:
For($i=1;$i -le $numbers.count;$i++)
That starts 1, and since arrays start at 0 this will get you going with the second record. Then in the scriptblock we do something like:
{
[array]$Results += [math]::abs($numbers[$i] - $numbers[($i-1)])
}
That will get the differences for you, then to display them you can do something like:
"the results = " + ($Results -join ", ")
$sum = $Results|Measure -sum|select -expand Sum
So you put that all together and get
For($i=1;$i -le $numbers.count;$i++){
[array]$Results += [math]::abs($numbers[$i] - $numbers[($i-1)])
}
"the results = " + ($Results -join ", ")
$sum = $Results|Measure -sum|select -expand Sum
Use a for loop, use the length of your $numbers array to know when to stop.
$numbers = #(33,45,66,47,50)
$sum = 0
for($cur=1;$cur -lt $numbers.Length; $cur += 1){
$sum += [math]::abs($numbers[$cur]-$numbers[$cur-1]);
}
$sum

Powershell Issues Comparing two Arrays using Nested Foreach loop

I'am not able to have this script to working as expected. Please see below
i have two arrays $newusers and $oldusers with below data in each
$newusers = fadbd34|Alan Simon|Jones,ken A
fadbk45|Alice Lund|Dave,John h
fadoo78|Nathan Hugh|Trot,Carol M
fadt359|Jon Hart|Jones,Karen D
fafyl38|Miley Mcghee|Main,Josh D
abbrt86|Andrew Hayden|Mary,Martin G
frt5096|Andrew Cork|Kain,Martha E
ikka155|Andrew Mullen|Raymond, Gavin G
Note: Please observe the last 3 users from $newusers are not there in $oldusers
$oldusers = fadbd34|Alan Simon|11754
fadbk45|Alice Lund|11755
fadoo78|Nathan Hugh|11755
fadt359|Jon Hart|11755
fafyl38|Miley Mcghee|11732
Now, i'am trying to write a script that checks if the first field(Userid) from $newusers is traced in $oldusers
then join fields $newusers[0],$newusers[1],$oldusers[2], $newusers[2]into $Activeusers array and for the new userid's not found in $oldusers join $newusers[0], $newusers[1], $newusers[2] into $Inactiveusers array. I'am getting incorrect results. Below is so far i can came up with.
$Activeusers = #()
$Inactiveusers = #()
foreach ($nrow in $newusers) {
foreach ($orow in $oldusers){
($idNew,$newusrname,$mgr) = $newrow.split('|')
($idOld,$oldusrname,$costcntr) = $oldrow.split('|')
if ( $idOld[0] -ieq $idOld[0]){
$Activeusers += [string]::join('|',($idNew[0],$nusrname[1],$cstcntr[2],$mgr[2]))
} else {
$Inactiveusers += [string]::join('|',($idNew[0],$nusrname[1],$mgr[2]))
}
}
}
The issue is with how you are looping through the records. You are comparing the first record in the new users list with all of the list of users in the second list. This basic if/else statement causes you to get the following result. for ex.:
Loop 1: compare:
fadbd34 = fadbd34 -> Active User
Loop 2: compare:
fadbd34 = fadbk45 -> Inactive User
Loop 3: compare:
fadbd34 = fadoo78 -> Inactive User
...
This causes you to get 1 correct active user, and a list of 5 inactive users. Every time the user does not match a user in the old list, it stores it as an inactive user.
The code that I got working (After cleaning it up, removing the unnecessary array references (If you split the string, and store it in a variable, you don't need an array reference), fixing variable names, and changing the if statement to compare $idOld to $idNew) is this:
$newusers =
"fadbd34|Alan Simon|Jones,ken A",
"fadbk45|Alice Lund|Dave,John h",
"fadoo78|Nathan Hugh|Trot,Carol M",
"fadt359|Jon Hart|Jones,Karen D",
"fafyl38|Miley Mcghee|Main,Josh D",
"abbrt86|Andrew Hayden|Mary,Martin G",
"frt5096|Andrew Cork|Kain,Martha E",
"ikka155|Andrew Mullen|Raymond, Gavin G"
$oldusers =
"fadbd34|Alan Simon|Jones,ken A",
"fadbk45|Alice Lund|Dave,John h",
"fadoo78|Nathan Hugh|Trot,Carol M",
"fadt359|Jon Hart|Jones,Karen D",
"fafyl38|Miley Mcghee|Main,Josh D",
"abbrt86|Andrew Hayden|Mary,Martin G"
$Activeusers = #()
$Inactiveusers = #()
foreach ($nrow in $newusers) {
$Active = $false
($idNew,$newusrname,$mgr) = $nrow.split('|')
foreach ($orow in $oldusers){
($idOld,$oldusrname,$costcntr) = $orow.split('|')
if ( $idNew -ieq $idOld){
$Activeusers += [string]::join('|',($idNew,$nusrname,$costcntr,$mgr))
$Active = $true
}
}
if (!$Active)
{
$Inactiveusers += [string]::join('|',($idNew,$newusrname,$mgr))
}
}