Return object from array with highest value - powershell

I want to return an object from an array who's property has the highest value. Currently I am doing the following
Get-VM | Sort-Object -Property ProvisionedSpaceGB | Select-Object -Last 1
This works but is inefficient. I don't need the entire array sorted, I just need the object with largest value. Ideally I would use something like
Get-VM | Measure-Object -Property ProvisionedSpaceGB -Maximum
but this only returns the value of the object property, not the entire object. Is there a way to have measure-object return the base object?

Not directly. Measure-Object is intended to be an easy way to grab such values, not their input objects. You could get the maximum from Measure-Object and then compare against the array, but it takes a few steps:
$array = Get-VM
$max = ($array | measure-object -Property ProvisionedSpaceGB -maximum).maximum
$array | ? { $_.ProvisionedSpaceGB -eq $max}
You could also forgo Measure-Object entirely and iterate through the set, replacing the maximum and output as you go.
$max = 0
$array | Foreach-Object
{
if($max -le $_.ProvisionedSpaceGB)
{
$output = $_
$max = $_.ProvisionedSpaceGB
}
}
$output
This is a little dirtier so as to always return a single value. It would need a minor adjustment if you were to reuse it in a case where there may be multiple values that have the same maximum (filesize lengths when using Get-ChildItem, for example). It will replace $output with the latter iterate in a case where two or more objects have the same value for ProvisionedSpaceGB. You could turn $output into a collection easily enough to fix that.
I prefer the former solution myself, but I wanted to offer a different way to think about the problem.

You can use this:
$array = Get-VM | Sort-Object -Property ProvisionedSpaceGB -Descending
$array[0]

Related

Find the min element of Hashtable (Values are - DateTime) on PowerShell

There is a hashtable of view:
key(string) - value (dateTime)
Have to find the min value among Values (dateTime-s).
Can't find generic method to find such a value. The only way is smth like
$first_file_date = $dates_hash.Values | Measure-Object -Minimum -Maximum
Get-Date ($first_file_date);
Though visibly I get the result ($first_file_date) the actual value casts to GenericObjectMeasureInfo type and I can't cast it back to DateTime to work further.
Any ideas?
The value you're interested in is stored in the Minimum and Maximum properties of the object returned by Measure-Object:
$measurement = $dates_hash.Values | Measure-Object -Minimum -Maximum
# Minimum/oldest datetime value is stored here
$measurement.Minimum
# Maximum/newest datetime value is stored here
$measurement.Maximum
Use ForEach-Object or Select-Object if you want the raw value in a single pipeline:
$oldest = $dates_hash.Values | Measure-Object -Minimum | ForEach-Object -MemberName Minimum
# or
$oldest = $dates_hash.Values | Measure-Object -Minimum | Select-Object -ExpandProperty Minimum
To complement Mathias R. Jessen's helpful answer with an alternative solution based on LINQ:
# Sample hashtable.
$hash = #{
foo = (Get-Date)
bar = (Get-Date).AddDays(-1)
}
# Note that the minimum is sought among the hash's *values* ([datetime] instances)
# The [datetime[] cast is required to find the appropriate generic overload.
[Linq.Enumerable]::Min([datetime[]] $hash.Values)
Use of LINQ from PowerShell is generally cumbersome, unfortunately (see this answer). GitHub proposal #2226 proposes improvements.
Just use Sort-Object for this:
$dates_hash = #{
"a" = (Get-Date).AddMinutes(4)
"b" = (Get-Date).AddMinutes(5)
"c" = (Get-Date).AddMinutes(2)
"d" = (Get-Date).AddMinutes(5)
"e" = (Get-Date).AddMinutes(1)
"f" = (Get-Date).AddMinutes(6)
"g" = (Get-Date).AddMinutes(8)
}
$first_file_date = $dates_hash.Values | Sort-Object | Select-Object -First 1
Or if you want the whole object:
$first_file = $dates_hash.GetEnumerator() | Sort-Object -Property "Value" | Select-Object -First 1

Custom object array element with minimum / maximum value of some property

$CustomObjects = Get-Random -Count 7 -InputObject #(0..300) | ForEach-Object {$i = 0} {
[PSCustomObject]#{
id = ($i++)
value = $_
}
}
$min, $max = & {$Args[0].Minimum, $Args[0].Maximum} ($CustomObjects | ForEach-Object value | Measure-Object -Minimum -Maximum)
$CustomObjects | Format-Table id, value -RepeatHeader
$CustomObjects | Where-Object {$_.value -eq $min} | Format-Table id, value
$CustomObjects | Where-Object {$_.value -eq $max} | Format-Table id, value
Are there more interesting options for finding the minimum / maximum?
We could come up with a long list of valid PowerShell statements that all look slightly different and would yield the same result, but there's basically 2 ways:
Sorting
Keeping count
Obtaining min/max through sorting is exactly what it sounds like:
$min,$max = #($CustomObjects |Sort Value)[0,-1].Value
# or
$min,$max = #($CustomObjects.Value |Sort)[0,-1]
Very succinct, easy to express - but might turn out to be slow on large collections
Obtaining min/max through keeping count is exactly what Measure-Object does:
$min,$max = [int]::MaxValue,[int]::MinValue
$CustomObjects |ForEach-Object {
$min = [Math]::Min($_.Value, $min)
$max = [Math]::Max($_.Value, $max)
}
Not as fancy, but faster for large collections
Mathias R. Jessen's helpful answer shows elegant - but potentially wasteful in terms of memory - solutions (Sort-Object of necessity needs to collect all input objects in memory so as to be able to determine the sort order).
Your own, Measure-Object-based approach takes advantage of the memory-throttling property of the pipeline.
Due to use of PowerShell's pipeline, both approaches are likely to be slow, however - see this answer for a discussion.
A simple optimization of your approach is to make the ForEach-Object value pipeline segment unnecessary by passing -Property value to Measure-Object:
$CustomObjects | Measure-Object -Property value -Minimum -Maximum
The Microsoft.PowerShell.Commands.GenericMeasureInfo returned by this command has .Minimum and .Maximum properties, so there's no strict reason to create separate $min and $max variables, but if you wanted to:
$measureResult = $CustomObjects | Measure-Object -Property value -Minimum -Maximum
$min, $max = $measureResult.Minimum, $measureResult.Maximum
If you'd rather not create an auxiliary variable, you can use the following, which uses the .ForEach() array method:
$min, $max = ($CustomObjects | Measure-Object -Property value -Minimum -Maximum).
ForEach({ $_.Minimum, $_.Maximum })

Why does select-object -first 1 not return an array for consistency?

When I pipe some objects to select-object -first n it returns an array except if n is 1:
PS C:\> (get-process | select-object -first 1).GetType().FullName
System.Diagnostics.Process
PS C:\> (get-process | select-object -first 2).GetType().FullName
System.Object[]
For consistency reasons, I'd have expected both pipelines to return an array.
Apparently, PowerShell chooses to return one object as object rather than as an element in an array.
Why is that?
Why questions are generally indeterminate in cases like this, but it mostly boils down to:
Since we asked for the "-first 1" we would expect a single item.
If we received an array/list we would still need to index the first one to obtain just that one, which is pretty much what "Select-Object -First 1" is designed to do (in that case.)
The result can always be wrapped in #() to force an array -- perhaps in the case where we've calculated "-First $N" and don't actually know (at that moment in the code) that we might receive only 1.
The designer/developer thought it should be that way.
It's #3 that keeps it from being an issue:
$PSProcess = #(Get-Process PowerShell | Select -First 1)
...this will guarantee $PSProcces is an array no matter what the count.
It even works with:
$n = Get-Random 3
#(Get-Process -first $n) # $n => 0, 1, or 2 but always returns an array.
The pipeline will return the [System.Diagnostics.Process] object. In your first example it's only one object. The second one is an [System.Object[]] array of the [System.Diagnostics.Process].
$a = (get-process | select-object -first 1)
$a | Get-Member
$b = (get-process | select-object -first 2)
,$b | Get-Member

Select-Object of multiple properties

I am trying to find an elegant way to put the metadata of a table of type System.Data.DataTable into a multi-dimensional array for easy reference in my program. My approach to the issue so far seems tedious.
Assuming $DataTable being the DataTable in question
What I tried to do so far was:
$Types = $DataTable.Columns | Select-Object -Property DataType
$Columns= $DataTable.Columns | Select-Object -Property ColumnName
$Index = $DataTable.Columns | Select-Object -Property ordinal
$AllowNull = $DataTable.Columns | Select-Object -Property AllowDbNull
Then painfully going through each array, pick up individual items and put them in my multi-dimensional array $TableMetaData.
I read in the documentation of Select-Object and it seems to me that only 1 property can be selected at 1 time? I think I should be able to do all the above more elegantly and store the information in $TableMetaData.
Is there a way to easily pick up multiple properties and put them in a multi-dimensional array in 1 swoop?
I read the documentation of Select-Object and it seems to me that only 1 property can be selected at 1 time?
This is not true, Select-Object can take any number of arguments to the -Property parameter
$ColumnInfo = $DataTable.Columns | Select-Object -Property DataType,ColumnName,ordinal,AllowDbNull
Now $ColumnInfo will contain one object for each column, having all 4 properties.
Rather than using a multi-dimensional array, you should consider using a hashtable (#{}, an unordered dictionary):
$ColumnInfo = $DataTable.Columns | ForEach-Object -Begin { $ht = #{} } -Process {
$ht[$_.ColumnName] = $_
} -End { return $ht }
Here, we create an empty hashtable $ht (the -Begin block runs just once), then store each column object in $ht using the ColumnName as the key, and finally return $ht, storing it in $ColumnInfo.
Now you can reference metadata about each column by Name:
$ColumnInfo.Column2
# or
$ColumnInfo["Column2"]
One easy way to do this is to create an "empty" variable with Select-Object. Here is a sample command:
$DataTableReport = "" | Select-Object -Property DataType, ColumnName, ordinal, AllowDbNull
Then, link the $DataTableReport to the $Types, $Columns, $Index, and the $AllowNull properties as shown below:
$DataTableReport.Types = $DataTable.DataType
$DataTableReport.Columns = $DataTable.ColumnName
$DataTableReport.Index = $DataTable.ordinal
$DataTableReport.AllowNull = $DataTable.AllowDbNull
Finally, call the DataTableReport variable.
$DataTableReport # will display all the results in a tabular form.

Powershell Select-Object from array not working

I am trying to seperate values in an array so i can pass them to another function.
Am using the select-Object function within a for loop to go through each line and separate the timestamp and value fields.
However, it doesn't matter what i do the below code only displays the first select-object variable for each line. The second select-object command doesn't seem to work as my output is a blank line for each of the 6 rows.
Any ideas on how to get both values
$ReportData = $SystemStats.get_performance_graph_csv_statistics( (,$Query) )
### Allocate a new encoder and turn the byte array into a string
$ASCII = New-Object -TypeName System.Text.ASCIIEncoding
$csvdata = $ASCII.GetString($ReportData[0].statistic_data)
$csv2 = convertFrom-CSV $csvdata
$newarray = $csv2 | Where-Object {$_.utilization -ne "0.0000000000e+00" -and $_.utilization -ne "nan" }
for ( $n = 0; $n -lt $newarray.Length; $n++)
{
$nTime = $newarray[$n]
$nUtil = $newarray[$n]
$util = $nUtil | select-object Utilization
$util
$tstamp = $nTime | select-object timestamp
$tstamp
}
Let me slightly modify the processing code, if it will help.
$csv2 |
Where-Object {$_.utilization -ne "0.0000000000e+00" -and $_.utilization -ne "nan" } |
Select-Object Utilization,TimeStamp
It will produce somewhat different output, but that should be better for working with.
The result are objects with properties Utilization and TimeStamp. You can pass them to the another function as you mention.
Generally it is better to use pipes instead of for loops. You don't need to care about indexes and it works with arrays as well as with scalar values.
If my updated code won't work: is the TimeStamp property really filled with any value?