Query PSCustomObject Array for row with largest value - arrays

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.

Related

Format an array of hashtables as table with Powershell

I wish to display an array of hastables as a Table. I found several threads dealing with this issue, but so far no solution has worked for me (see here and here).
My array consists of 18 hashtables in this form:
Array = #(
#{Company=1}
#{Company=2}
#{Company=3}
#{Contact=X}
#{Contact=Y}
#{Contact=Y}
#{Country=A}
#{Country=B}
#{Country=C}
)
I would like to get the following output:
Company Contact Country
------------------------------
1 X A
2 Y B
3 Z C
I've tried the following:
$Array | ForEach-Object { New-Object -Type PSObject -Property $_ } | Format-Table
That displays the following:
Company
----------
1
2
3
A Format-List works better; I then get this:
Company: 1 2 3
Contact: X Y Z
Country: A B C
Is there any way to accomplish my desired output?
You can do the following if you want to work with CSV data and custom objects:
$Array = #(
#{Company=1}
#{Company=2}
#{Company=3}
#{Contact='X'}
#{Contact='Y'}
#{Contact='Z'}
#{Country='A'}
#{Country='B'}
#{Country='C'}
)
$keys = $Array.Keys | Get-Unique
$data = for ($i = 0; $i -lt $Array.($Keys[0]).Count; $i++) {
($Keys | Foreach-Object { $Array.$_[$i] }) -join ','
}
($Keys -join ','),$data | ConvertFrom-Csv | Format-Table
Explanation:
Since $Array is an array of hash tables, then the .Keys property will return all the keys of those hash tables. Since we only care about unique keys when building our object, Get-Unique is used to remove duplicates.
$Array.($Keys[0]).Count counts the number of items in a group. $Keys[0] here will be Company. So it returns the number of hash tables (3) that contain the key Company.
$Array.Company for example returns all values from the hash tables that contain the key Company. The Foreach-Object loops through each unique key's value at a particular index ($i). Once each key-value is read at a particular index, the values are joined by a comma.
When the loop completes, ($Keys -join ','),$data outputs the data in CSV format, which is piped into ConvertFrom-Csv to create a custom object.
Note: that if your data contains commas, you may want to consider the alternative method below.
Alernatively, you can work with hash tables and custom objects with the following:
$Array = #(
#{Company=1}
#{Company=2}
#{Company=3}
#{Contact='X'}
#{Contact='Y'}
#{Contact='Z'}
#{Country='A'}
#{Country='B'}
#{Country='C'}
)
$keys = $Array.Keys | Get-Unique
$data = for ($i = 0; $i -lt $Array.($Keys[0]).Count; $i++) {
$hash = [ordered]#{}
$Keys | Foreach-Object { $hash.Add($_,$Array.$_[$i]) }
[pscustomobject]$hash
}
$data | Format-Table

Efficient way to remove duplicates from large 2D arrays in PowerShell

I have a large set of data roughly 10 million items that I need to process efficiently and quickly removing duplicate items based on two of the six column headers.
I have tried grouping and sorting items but it's horrendously slow.
$p1 = $test | Group-Object -Property ComputerSeriaID,ComputerID
$p2 = foreach ($object in $p1.group) {
$object | Sort-Object -Property FirstObserved | Select-Object -First 1
}
The goal would be to remove duplicates by assessing two columns while maintaining the oldest record based on first observed.
The data looks something like this:
LastObserved : 2019-06-05T15:40:37
FirstObserved : 2019-06-03T20:29:01
ComputerName : 1
ComputerID : 2
Virtual : 3
ComputerSerialID : 4
LastObserved : 2019-06-05T15:40:37
FirstObserved : 2019-06-03T20:29:01
ComputerName : 5
ComputerID : 6
Virtual : 7
ComputerSerialID : 8
LastObserved : 2019-06-05T15:40:37
FirstObserved : 2019-06-03T20:29:01
ComputerName : 9
ComputerID : 10
Virtual : 11
ComputerSerialID : 12
You might want to clean up your question a little bit, because it's a little bit hard to read, but I'll try to answer the best I can with what I can understand about what you're trying to do.
Unfortunately, with so much data there's no way to do this quickly. String Comparison and sorting are done by brute force; there is no way to reduce the complexity of comparing each character in one string against another any further than measuring them one at a time to see if they're the same.
(Honestly, if this were me, I'd just use export-csv $object and perform this operation in excel. The time tradeoff to scripting something like this only once just wouldn't be worth it.)
By "Items" I'm going to assume that you mean rows in your table, and that you're not trying to retrieve only the strings in the rows you're looking for. You've already got the basic idea of select-object down, you can do that for the whole table:
$outputFirstObserved = $inputData | Sort-Object -Property FirstObserved -Unique
$outputLastObserved = $inputData | Sort-Object -Property LastObserved -Unique
Now you have ~20 million rows in memory, but I guess that beats doing it by hand. All that's left is to join the two tables. You can download that Join-Object command from the powershell gallery with Install-Script -Name Join and use it in the way described. If you want to do this step yourself, the easiest way would be to squish the two tables together and sort them again:
$output = $outputFirstObserved + $outputLastObserved
$return = $output | Sort-Object | Get-Unique
Does this do it? It keeps the one it finds first.
$test | sort -u ComputerSeriaID, ComputerID
I created this function to de-duplicate my multi-dimensional arrays.
Basically, I concatenate the contents of the record, add this to a hash.
If the concatenate text already exists in the hash, don't add it to the array to be returned.
Function DeDupe_Array
{
param
(
$Data
)
$Return_Array = #()
$Check_Hash = #{}
Foreach($Line in $Data)
{
$Concatenated = ''
$Elements = ($Line | Get-Member -MemberType NoteProperty | % {"$($_.Name)"})
foreach($Element in $Elements)
{
$Concatenated += $line.$Element
}
If($Check_Hash.$Concatenated -ne 1)
{
$Check_Hash.add($Concatenated,1)
$Return_Array += $Line
}
}
return $Return_Array
}
Try the following script.
Should be as fast as possible due to avoiding any pipe'ing in PS.
$hashT = #{}
foreach ($item in $csvData) {
# Building hash table key
$key = '{0}###{1}' -f $item.ComputerSeriaID, $item.ComputerID
# if $key doesn't exist yet OR when $key exists and "FirstObserverd" is less than existing one in $hashT (only valid when date provided in sortable format / international format)
if ((! $hashT.ContainsKey($key)) -or ( $item.FirstObserved -lt $hashT[$key].FirstObserved )) {
$hashT[$key] = $item
}
}
$result = $hashT.Values

Select-String bug splitting up lines [duplicate]

I'm using Powershell to set up IIS bindings on a web server, and having a problem with the following code:
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
if ($serverIps.length -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
$primaryIp = $serverIps[0]
$secondaryIp = $serverIps[1]
If there's 2+ IPs on the server, fine - Powershell returns an array, and I can query the array length and extract the first and second addresses just fine.
Problem is - if there's only one IP, Powershell doesn't return a one-element array, it returns the IP address (as a string, like "192.168.0.100") - the string has a .length property, it's greater than 1, so the test passes, and I end up with the first two characters in the string, instead of the first two IP addresses in the collection.
How can I either force Powershell to return a one-element collection, or alternatively determine whether the returned "thing" is an object rather than a collection?
Define the variable as an array in one of two ways...
Wrap your piped commands in parentheses with an # at the beginning:
$serverIps = #(gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort)
Specify the data type of the variable as an array:
[array]$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
Or, check the data type of the variable...
IF ($ServerIps -isnot [array])
{ <error message> }
ELSE
{ <proceed> }
Force the result to an Array so you could have a Count property. Single objects (scalar) do not have a Count property. Strings have a length property so you might get false results, use the Count property:
if (#($serverIps).Count -le 1)...
By the way, instead of using a wildcard that can also match strings, use the -as operator:
[array]$serverIps = gwmi Win32_NetworkAdapterConfiguration -filter "IPEnabled=TRUE" | Select-Object -ExpandProperty IPAddress | Where-Object {($_ -as [ipaddress]).AddressFamily -eq 'InterNetwork'}
You can either add a comma(,) before return list like return ,$list or cast it [Array] or [YourType[]] at where you tend to use the list.
If you declare the variable as an array ahead of time, you can add elements to it - even if it is just one...
This should work...
$serverIps = #()
gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort | ForEach-Object{$serverIps += $_}
You can use Measure-Object to get the actual object count, without resorting to an object's Count property.
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
if (($serverIps | Measure).Count -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
Return as a referenced object, so it never converted while passing.
return #{ Value = #("single data") }
I had this problem passing an array to an Azure deployment template. If there was one object, PowerShell "converted" it to a string. In the example below, $a is returned from a function that gets VM objected according to the value of a tag. I pass the $a to the New-AzureRmResourceGroupDeployment cmdlet by wrapping it in #(). Like so:
$TemplateParameterObject=#{
VMObject=#($a)
}
New-AzureRmResourceGroupDeployment -ResourceGroupName $RG -Name "TestVmByRole" -Mode Incremental -DeploymentDebugLogLevel All -TemplateFile $templatePath -TemplateParameterObject $TemplateParameterObject -verbose
VMObject is one of the template's parameters.
Might not be the most technical / robust way to do it, but it's enough for Azure.
Update
Well the above did work. I've tried all the above and some, but the only way I have managed to pass $vmObject as an array, compatible with the deployment template, with one element is as follows (I expect MS have been playing again (this was a report and fixed bug in 2015)):
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Web.Extensions")
foreach($vmObject in $vmObjects)
{
#$vmTemplateObject = $vmObject
$asJson = (ConvertTo-Json -InputObject $vmObject -Depth 10 -Verbose) #-replace '\s',''
$DeserializedJson = (New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer -Property #{MaxJsonLength=67108864}).DeserializeObject($asJson)
}
$vmObjects is the output of Get-AzureRmVM.
I pass $DeserializedJson to the deployment template' parameter (of type array).
For reference, the lovely error New-AzureRmResourceGroupDeployment throws is
"The template output '{output_name}' is not valid: The language expression property 'Microsoft.WindowsAzure.ResourceStack.Frontdoor.Expression.Expressions.JTokenExpression'
can't be evaluated.."
There is a way to deal with your situation. Leave most of you code as-is, just change the way to deal with the $serverIps object. This code can deal with $null, only one item, and many items.
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
# Always use ".Count" instead of ".Length".
# This works on $null, only one item, or many items.
if ($serverIps.Count -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
# Always use foreach on a array-possible object, so that
# you don't have deal with this issue anymore.
$serverIps | foreach {
# The $serverIps could be $null. Even $null can loop once.
# So we need to skip the $null condition.
if ($_ -ne $null) {
# Get the index of the array.
# The #($serverIps) make sure it must be an array.
$idx = #($serverIps).IndexOf($item)
if ($idx -eq 0) { $primaryIp = $_ }
if ($idx -eq 1) { $secondaryIp = $_ }
}
}
In PowerShell Core, there is a .Count property exists on every objects. In Windows PowerShell, there are "almost" every object has an .Count property.

How to create and populate an array in Powershell based on a dynamic variable?

I've been struggling with this for a couple of days, and I'm not sure how to conquer it. I need to do the following:
Import a csv of users with the following values:
ID, Name, Region
Create an array based on the Region values that I can then use to populate with ID's and Names with that region, ie.
Array_SEA
AA_SCOM, Adam Andrews, SEA
Array_OAK
BB_SCOM, Bob Barker, OAK
Here's the code I've got right now:
$list2 = ipcsv .\TSE_Contact_List.csv | sort-object BU
$arraylist =#()
foreach ($vitem in $list2)
{
$arraylist += New-Object PsObject -Property #{'Array' = "Array_" + $vitem.bu}
}
foreach ($varray in $arraylist)
{
$arr = new-variable -Name $varray
$arr.value += $varray.array
$arr
}
This produces the following error for records with a duplicate regions:
New-Variable: A variable with name '#{Array=Array_SCA}' already exists.
I'm also getting the following when it tries to add values:
Property 'value' cannot be found on this object; make sure it exists and is settable.
I get that I'm not actually creating arrays in the second section, but I'm not sure how to pass the output of the variable to an array name without turning the variable declaration into the array name, if that makes sense.
I've tried the following with hash tables, and it gets closer:
$list2 = ipcsv .\TSE_Contact_List.csv | sort-object BU
$arraylist =#{}
foreach ($vitem in $list2){$arraylist[$vitem.bu] = #()}
foreach ($record in $list2)
{
$arraylist[$vitem.bu] += ($record.SCOMID,$record.Name,$record.BU)
Write-host "Array: "
$arraylist[$vitem.bu]
write-host ""
}
The output on this shows no errors, but it just keeps showing the added fields for all of the records for each iteration of the list, so I don't think that it's actually assigning each unique BU to the array name.
I like the hashtable-approach, but I would finetune it a little. Try:
$list2 = ipcsv .\TSE_Contact_List.csv | sort-object BU
$arraylist = #{}
foreach ($vitem in $list2){
if($arraylist.ContainsKey($vitem.BU)) {
#Array exists, add item
$arraylist[($vitem.BU)] += $vitem
} else {
#Array not found, creating it
$arraylist[($vitem.BU)] = #($vitem)
}
}
#TEST: List arrays and number of entries
$arraylist.GetEnumerator() | % {
"Array '$($_.Key)' has $($_.Value.Count) items"
}
You could also use Group-Object like:
$list2 = ipcsv .\TSE_Contact_List.csv | Group-Object BU
#TEST: List groups(regions) and number of entries
$list2 | % {
"Region '$($_.Name)' has $(#($_.Group).Count) items"
}

How to store outlook email body in array - Powershell?

the script below reads my outlook emails but how do I access the output. I'm new too Powershell and I'm still getting used to certain things. I just want to get the body of 10 unread outlook emails and store them in an Array called $Body.
$olFolderInbox = 6
$outlook = new-object -com outlook.application;
$ns = $outlook.GetNameSpace("MAPI");
$inbox = $ns.GetDefaultFolder($olFolderInbox)
#checks 10 newest messages
$inbox.items | select -first 10 | foreach {
if($_.unread -eq $True) {
$mBody = $_.body
#Splits the line before any previous replies are loaded
$mBodySplit = $mBody -split "From:"
#Assigns only the first message in the chain
$mBodyLeft = $mbodySplit[0]
#build a string using the –f operator
$q = "From: " + $_.SenderName + ("`n") + " Message: " + $mBodyLeft
#create the COM object and invoke the Speak() method
(New-Object -ComObject SAPI.SPVoice).Speak($q) | Out-Null
}
}
This may not be a factor here, since you're looping through only ten elements, but using += to add elements to an array is very slow.
Another approach would be to output each element within the loop, and assign the results of the loop to $body. Here's a simplified example, assuming that you want $_.body:
$body = $inbox.items | select -first 10 | foreach {
if($_.unread -eq $True) {
$_.body
}
}
This works because anything that is output during the loop will be assigned to $body. And it can be much faster than using +=. You can verify this for yourself. Compare the two methods of creating an array with 10,000 elements:
Measure-Command {
$arr = #()
1..10000 | % {
$arr += $_
}
}
On my system, this takes just over 14 seconds.
Measure-Command {
$arr = 1..10000 | % {
$_
}
}
On my system, this takes 0.97 seconds, which makes it over 14 times faster. Again, probably not a factor if you are just looping through 10 items, but something to keep in mind if you ever need to create larger arrays.
define $body = #(); before your loop
Then just use += to add the elements
Here's another way:
$body = $inbox.Items.Restrict('[Unread]=true') | Select-Object -First 10 -ExpandProperty Body

Resources