Sorting Hashtable with Arrays as Values

Description . I'm creating a PowerShell-script that looks for files, then gives them unique names, copies them, and then checks them using a hash calculation - I decided to split the script in functions for each step, so it's easier to maintain the whole thing. To get all the values ​​from one function to another, I decided to use [hashtable]$FooBar

- internally $FooBar

, there are several arrays like FullName

or OutputPath

(which can change per file as they will be copied into subfolders named yyyy-mm-dd

). All arrays are related to each other (which means index 1 contains all the values ​​for the first file, index 2 contains the values ​​for the second file, ...) and that works fine for now.

Brief simplified visualization:

$FooBar = @{}
$FooBar.FullName = @()
$FooBar.Size = @()
$FooBar.Ext = @()
Get-ChildItem | ForEach-Object {
    $FooBar.FullName += $_.FullName
    $FooBar.Size += $_.Length
    $FooBar.Ext += $_.Extension
}

      

However, now I need to sort them by one set of values ​​of one of the arrays, for example. the size. Or rendered again:

# From:
$FooBar
Name                           Value
----                           -----
fullname                       {D:\AAA.XYZ, D:\BBB.ZYX, D:\CCC.YZX}
size                           {222, 111, 555}
extension                      {.XYZ, .ZYX, .YZX}

# To:
$FooBar = $FooBar | Sort-Object -Property Size -Descending
$FooBar
Name                           Value
----                           -----
fullname                       {D:\CCC.YZX, D:\AAA.XYZ, D:\BBB.ZYX}
size                           {555, 222, 111}
extension                      {.YZX, .XYZ, .ZYX}

      

I tried $FooBar.GetEnumerator() | Sort-Object -Property Size

it but it doesn't change anything. Google has included suggestions on how to sort an array of hash tables, but in my case it's the other way around and I can't think of it because I don't even understand why this is the problem in the first place.

So my question is , is there a way to sort all the arrays within the hash table using a set of values ​​from one of the arrays? I cannot think it over.

Disclaimer: I am PowerShell-autodidact with no reasonable background in scripting / programming, so it is entirely possible that my "include everything in one hash table" - the solution will not work at all or may be extremely inefficient - if so, please tell me ...

+3


source to share


3 answers


The easiest way to do what I believe you are trying to do is Select-Object

$fooBar = Get-ChildItem | Select-Object FullName, Size, Extension

      

This will create an array of new objects that only have the properties you want. The reason this works and your method is missing is because Sort-Object works with properties and the property you specify is behind multiple layers.

If you need more flexibility than just the exact properties, you can create your own like this.

$fooBar = Get-ChildItem | Select-Object @{Name = 'SizeMB'; Expression = {$_.Size / 1MB}}

      

Or manually create new properties using an accelerator like [PSCustomObject]

:

$fooBar = Get-ChildItem | ForEach-Object {
    [PSCustomObject]@{
        FullName = $_.FullName
        Extension = $_.Extension
        Size = $_.Size
    }
}

      

Update

If you need to add additional objects to an object after creating it, you have several options.

Add member

The most common method is to use a cmdlet Add-Member

.

$object | Add-Member -MemberType NoteProperty -Name NewProperty -Value 'MyValue'

$object

      



Something important to keep in mind is that, by default, this cmdlet returns nothing. So if you put the above statement at the end of the function and don't separately return the object, your function will return nothing. Make sure you use a parameter -PassThru

(this is also useful for chaining Add-Member

) or call the variable afterwards (like the example above)

Select-Object

You can select all previous properties when using calculated properties to add members. Be aware that due to the way it works Select-Object

, all methods from the original object are not carried over.

$fooBar | Select-Object *, @{Name = 'NewProperty'; Expression = {'MyValue'}}

      

psobject.Properties

This is my personal favorite, but it's limited to more recent versions of PowerShell and I haven't really seen it by anyone else.

$fooBar.psobject.Properties.Add([psnoteproperty]::new('NewProperty', 'MyValue'))
$fooBar

      

Each element type has its own constructor. You can also add methods to $fooBar.psobject.Methods

or or print to $fooBar.psobject.Members

. I like this method because it feels more explicit and something about adding members with members feels right.

Summary

You choose the method you prefer. I would recommend Add-Member

it if possible because it is the most commonly used so it has better readability and more people who can answer it.

I would also like to mention that it is usually best to avoid adding additional members if at all possible. The return value of a function should ideally be in a reliable form. If someone is using your function and they have to guess when a property or method will exist on your object, it is very difficult to debug. Obviously, this is not a hard and fast rule, but if you need to add a member, you should at least consider refactoring.

+5


source


For all practical purposes, I highly recommend that you just store the objects you want in one array, sort them once, and then refer to the individual properties of each object as needed:

$FooBar = Get-ChildItem |Sort-Object -Property Length

# Need the Extension property of the object at index 4?
$FooBar[4].Extension

      


To answer your real question:



Array.Sort()

has an overload
that takes keys and value arrays separately. You can make a copy of the array you want to sort for every other property you want to sort:

# Create hashtable of correlated arrays 
$FooBar = @{}
$FooBar.FullName = @()
$FooBar.Size = @()
$FooBar.Ext = @()
# Types cast explicitly to avoid Array.Sort() calling .CompareTo() on the boxing object
Get-ChildItem | ForEach-Object {
    $FooBar.FullName += [string]$_.FullName
    $FooBar.Size     += [int]$_.Length
    $FooBar.Ext      += [string]$_.Extension
}

# Define name of reference array property
$SortKey = 'Size'

# Sort all arrays except for the reference array
$FooBar.Keys |Where-Object {$_ -ne $SortKey} |ForEach-Object {
    # Copy reference values to new array
    $Keys = $FooBar[$SortKey].Clone()

    # Sort values in target array based on reference values
    [array]::Sort($Keys,$FooBar[$_])
}

# Finally sort the reference array
[array]::Sort($FooBar[$SortOn])

      

The above only works as long as the referenced array consists of value types

+4


source


PowerShell makes working with objects easy.

Try:

$FooBar = Get-Childitem
$FooBar | Get-Member

      

This will tell you what $Foobar

type objects really contain FileInfo

and DirectoryInfo

and shows you what is available Properties

.

$FooBarSortedBySizeDesc  = $FooBar | Sort-Object Length -Descending
$FooBarFullNamesOnly = $FooBar.FullName

      

+2


source







All Articles