Does there exist a designated (sub)index delimiter?
Asked Answered
R

3

2

Background

It is quite common in PowerShell to build a hash table to quickly access objects by a specific property, e.g. to base an index on the LastName:

$List =  ConvertFrom-Csv @'
Id, LastName, FirstName, Country
 1, Aerts,    Ronald,    Belgium
 2, Berg,     Ashly,     Germany
 3, Cook,     James,     England
 4, Duval,    Frank,     France
 5, Lyberg,   Ash,       England
 6, Fischer,  Adam,      Germany
'@

$Index = @{}
$List |ForEach-Object { $Index[$_.LastName] = $_ }

$Index.Cook

Id LastName FirstName Country
-- -------- --------- -------
3  Cook     James     England

In some cases it is required to build the index on two (or even more) properties, e.g. the FirstName and the LastName. For this you might create a multi dimensional key, e.g.:

$Index = @{}
$List |ForEach-Object {
     $Index[$_.FirstName] = @{}
     $Index[$_.FirstName][$_.LastName] = $_
}

$Index.James.Cook

Id LastName FirstName Country
-- -------- --------- -------
3  Cook     James     England

But it is easier (and possibly even faster) to just concatenate the two properties. If only for checking for the existence of the entry: $Index.ContainsKey('James').ContainsKey('Cook') where an error might occur if the FirstName doesn't exist.
To join the properties, it is required to use a delimiter between the property otherwise different property lists might end up as the same key. As this example: AshlyBerg and AshLyberg.

$Index = @{}
$List |ForEach-Object { $Index["$($_.FirstName)`t$($_.LastName)"] = $_ }

$Index."James`tCook"

Id LastName FirstName Country
-- -------- --------- -------
3  Cook     James     England

Note: the above are Minimal, Reproducible Examples. In real life, I come several times to the questions below, which includes generally joining objects where the background - and number of properties used in the index are variable.

Questions:

  1. Is it a good practice to join (concatenate) properties for such a situation?
  2. If yes, is there a (standard?) delimiter for this? (meaning a character -or a sequence of characters- that should never be used/exist in a property name)
Ronnieronny answered 27/6, 2022 at 12:45 Comment(2)
I think whether it's good practice to join the keys, depends on whether you only need to look up by full name. I've used the unit separator (ASCII 0x1F) as delimiter of string fields. List of control characters.Fouts
Another option is to generate a random delimiter for every table you built (much like the boundary delimiters in a multi-part email): [string]$delim = New-Guid; $List |%{ $Index[($_.FirstName,$_.LastName-join$delim)] = $_ }Cralg
M
5

There is no built-in separator for multi-component hashtable (dictionary) keys.

As for a custom separator: Your best bet for a character that is very unlikely to occur in the components themselves is NUL (the character with code point 0x0), which you can represent as "`0" in PowerShell. However, performing convention-based string operations on every lookup is awkward (e.g. $Index."James`0Cook") and generally only works if stringifying the key components is feasible - or if they're all strings to begin with, as in your example.

Using arrays for multi-component keys is syntactically preferable, but using collections generally does not work as-is, because .NET reference types in general do not compare distinct instances meaningfully, even if they happen to represent the same data - see this answer.

  • Note: The following assumes that the elements of collections serving as keys do compare meaningfully (are themselves strings or .NET value types or .NET reference types with custom equality logic). If that assumption doesn't hold, there's no robust general solution, but a best-effort approach based on CLIXML serialization shown in the linked answer may work, which you yourself have proposed.

zett42's helpful answer uses tuples, which do perform meaningful comparisons of distinct instances whose members contain equal data. However, the need to construct a tuple instance for each addition / modification / lookup is syntactically awkward (e.g.,
$Index.([Tuple]::Create('James', 'Cook'))

There is a way of making regular PowerShell arrays work as hastable keys, in a manner that only adds complexity to creating the hashtable (calling a constructor), while allowing regular array syntax for additions / updates and lookups (e.g., $Index.('James', 'Cook')).

  • Note: The following works equally with [ordered] hashtables, which, however, must be referred to by their true type name so as to be able to call a construct, namely [System.Collections.Specialized.OrderedDictionary].
    However, it does not work with generic dictionaries ([System.Collections.Generic.Dictionary[TKey, TValue]]).
# Sample objects for the hashtable.
$list =  ConvertFrom-Csv @'
Id, LastName, FirstName, Country
 1, Aerts,    Ronald,    Belgium
 2, Berg,     Ashly,     Germany
 3, Cook,     James,     England
 4, Duval,    Frank,     France
 5, Lyberg,   Ash,       England
 6, Fischer,  Adam,      Germany
'@

# Initialize the hashtable with a structural equality comparer, i.e.
# a comparer that compares the *elements* of the array and only returns $true
# if *all* compare equal.
# This relies on the fact that [System.Array] implements the
# [System.Collections.IStructuralEquatable] interface.
$dict = [hashtable]::new([Collections.StructuralComparisons]::StructuralEqualityComparer)

# Add entries that map the combination of first name and last name 
# to each object in $list.
# Note the regular array syntax.
$list.ForEach({ $dict.($_.FirstName, $_.LastName) = $_ })

# Use regular array syntax for lookups too.
# Note: CASE MATTERS
$dict.('James', 'Cook')

Important: The above performs case-SENSITIVE comparisons (as zett42's tuple solution does), unlike regular PowerShell hashtables.

Making the comparisons case-INSENSITIVE requires more work, because a custom implementation of the [System.Collections.IEqualityComparer] interface is required, namely a case-insensitive implementation of what [System.Collections.StructuralComparisons]::StructuralEqualityComparer provides:

# Case-insensitive IEqualityComparer implementation for 
# use of arrays as dictionary keys.
# Note: Dictionary keys cannot be $null, so there is no need for $null checks.
class CaseInsensitiveArrayEqualityComparer: System.Collections.IEqualityComparer {
   [bool] Equals([object] $o1, [object] $o2) {
      return ([System.Collections.IStructuralEquatable] [array] $o1).Equals([array] $o2, [System.StringComparer]::InvariantCultureIgnoreCase)
   }
   [int] GetHashCode([object] $o) {
     return ([System.Collections.IStructuralEquatable] [array] $o).GetHashCode([StringComparer]::InvariantCultureIgnoreCase)
   }
}

# Pass an instance of the custom equality comparer to the constructor.
$dict = [hashtable]::new([CaseInsensitiveArrayEqualityComparer]::new())

Note:

  • Santiago Squarzon discovered ([System.Collections.IStructuralEquatable] $o).GetHashCode([StringComparer]::InvariantCultureIgnoreCase) as a built-in way to get a hash code for an array based on its elements' case-insensitive hash codes.

  • The original solutions below calculate the array's case-insensitive hash code element by element, which is both more cumbersome and less efficient. Perhaps they are still of interest in general with respect to how hash codes are calculated.


Optional reading: element-by-element hash-code implementations:

# Case-insensitive IEqualityComparer implementation for arrays.
# See the bottom section of this answer for a better .NET 7+ alternative.
class CaseInsensitiveArrayEqualityComparer: System.Collections.IEqualityComparer {
  [bool] Equals([object] $o1, [object] $o2) {
    return ([System.Collections.IStructuralEquatable] [array] $o1).Equals([array] $o2, [System.StringComparer]::InvariantCultureIgnoreCase)
  }
  [int] GetHashCode([object] $o) {
    if ($o -isnot [Array]) { return $o.GetHashCode() }
    [int] $hashCode = 0
    foreach ($el in $o) {
      if ($null -eq $el) { 
        continue
      } elseif ($el -is [string]) {
        $hashCode = $hashCode -bxor $el.ToLowerInvariant().GetHashCode()
      } else {
        $hashCode = $hashCode -bxor $el.GetHashCode()
      }
    }
    return $hashCode
  }
}

$list =  ConvertFrom-Csv @'
Id, LastName, FirstName, Country
 1, Aerts,    Ronald,    Belgium
 2, Berg,     Ashly,     Germany
 3, Cook,     James,     England
 4, Duval,    Frank,     France
 5, Lyberg,   Ash,       England
 6, Fischer,  Adam,      Germany
'@

# Pass an instance of the custom equality comparer to the constructor.
$dict = [hashtable]::new([CaseInsensitiveArrayEqualityComparer]::new())

$list.ForEach({ $dict.($_.FirstName, $_.LastName) = $_ })

# Now, case does NOT matter.
$dict.('james', 'cook')

A note on the .GetHashCode() implementation in the custom comparer class above:

  • A custom .GetHashCode() implementation is required to return the same hash code (an [int] value) for all objects that compare as equal (that is, if $o1 -eq $o2 is $true, $o1.GetHashCode() and $o2.GetHashCode() must return the same value).

  • While hash codes aren't required to be unique (and cannot be in all cases), ideally as few objects as possible share the same hash code, as that reduces the number of so-called collisions, which decreases the lookup efficiency of hash tables - see the relevant Wikipedia article for background information.

  • The implementation above uses a fairly simple, -bxor-based (bitwise XOR) algorithm, which results in the same hash code for two arrays that have the same elements, but in different order.

    • The .GetHashCode() help topic shows more sophisticated approaches, including using an auxiliary tuple instance, as its hash code algorithm is order-aware - while simple, this approach is computationally expensive, and more work is needed for better performance. See the bottom section for a .NET 7+ option.

zett42's collision test code (adapted), which determines how many among 1000 arrays with a given number of elements that are random string values result in the same hash code, i.e. produce collisions, and calculates a collision percentage from that. If you need to improve the efficiency of the implementation above you can use this code to test it (possibly also to measure the tests' runtime to see how different implementations compare).

# Create an instance of the custom comparer defined above.
$cmp = [CaseInsensitiveArrayEqualityComparer]::new()

$numArrays = 1000
foreach ($elementCount in 2..5 + 10) {

  $numUniqueHashes = (
      1..$numArrays | 
      ForEach-Object { 
        $cmp.GetHashCode(@(1..$elementCount | ForEach-Object { "$(New-Guid)" })) 
      } |
      Sort-Object -Unique
    ).Count

  [pscustomobject] @{
    ElementCount = $elementCount
    CollisionPercentage = '{0:P2}' -f (($numArrays - $numUniqueHashes) / $numArrays)
  }

}

The about outputs 0% for all tests, so it seems that the -bxor approach is sufficient to prevent collisions, at least with random strings and without including variations of arrays that differ in element order only. Read on for a superior .NET 7+ solution.


Superior custom equality comparer implementation in .NET 7+ (requires at least a preview version of PowerShell 7.3):

zett42 points out that [HashCode]::Combine(), available in .NET 7+, allows for a more efficient implementation, as it:

  • is order-aware
  • allows determining a hash code for multiple values in a single operation.

Note:

  • The method is limited to at most 8 array elements - but for multi-component that should be enough.

  • The values to combine - the array elements in this case - must be passed as individual arguments to the methods - passing the array as a whole doesn't work as intended. This makes the implementation somewhat cumbersome.

# .NET 7+ / PowerShell 7.3+
# Case-insensitive IEqualityComparer implementation for arrays
# using [HashCode]::Combine() - limited to 8 elements.
class CaseInsensitiveArrayEqualityComparer: System.Collections.IEqualityComparer {
  [bool] Equals([object] $o1, [object] $o2) {
    return ([System.Collections.IStructuralEquatable] [array] $o1).Equals([array] $o2, [System.StringComparer]::InvariantCultureIgnoreCase)
  }
  [int] GetHashCode([object] $o) {
    if ($o -isnot [Array] -or 0 -eq $o.Count) { return $o.GetHashCode() }
    $o = $o.ForEach({ $_ -is [string] ? $_.ToLowerInvariant() : $_ })
    $hashCode = switch ($o.Count) {
      1 { [HashCode]::Combine($o[0]) }
      2 { [HashCode]::Combine($o[0], $o[1]) }
      3 { [HashCode]::Combine($o[0], $o[1], $o[2]) }
      4 { [HashCode]::Combine($o[0], $o[1], $o[2], $o[3]) }
      5 { [HashCode]::Combine($o[0], $o[1], $o[2], $o[3], $o[4]) }
      6 { [HashCode]::Combine($o[0], $o[1], $o[2], $o[3], $o[4], $o[5]) }
      7 { [HashCode]::Combine($o[0], $o[1], $o[2], $o[3], $o[4], $o[5], $o[6]) }
      8 { [HashCode]::Combine($o[0], $o[1], $o[2], $o[3], $o[4], $o[5], $o[6], $o[7]) }
      default { throw 'Not implemented for more than 8 array elements.' }
    }
    return $hashCode
  }
}

However, as zett42 points out, you can overcame the value-count limit by calling [HashCode]::Combine() iteratively, in a loop.

In the case of a case-insensitive implementation, that isn't too much overhead, given that you need a loop anyway, namely in order to call .ToLowerInvariant() on [string]-typed values (which is what the .ForEach() call above implicitly does).

Here is his implementation:

# .NET 7+ / PowerShell 7.3+
# Case-insensitive IEqualityComparer implementation for arrays
# using [HashCode]::Combine() *iteratively*, with *no* element-count limit.
class CaseInsensitiveArrayEqualityComparer: System.Collections.IEqualityComparer {
  [bool] Equals([object] $o1, [object] $o2) {
    return ([System.Collections.IStructuralEquatable] [array] $o1).Equals([array] $o2, [System.StringComparer]::InvariantCultureIgnoreCase)
  }
  [int] GetHashCode([object] $o) {
    if ($o -isnot [Array] -or 0 -eq $o.Count) { return $o.GetHashCode() }
    
    $hashCode = 0
    $o.ForEach({ 
        $value = $_ -is [string] ? $_.ToLowerInvariant() : $_
        $hashCode = [HashCode]::Combine( $hashCode, $value )
    })

    return $hashCode
  }
}
Moult answered 28/6, 2022 at 21:51 Comment(16)
Thanks for catching that, @Fouts - I actually meant to use -bxor to begin with - -bor performs much worse. In fact, -bxor seems to make all collisions go away in your test. Please see my update, and let me know if I'm missing something.Moult
@Fouts -the tests are still good to have in general (sanity check, measuring runtime to see how fast the implementation is, ...). Thanks for the tip re [HashCode]::Combine() - I've added a bottom section to the answer that show its use and notes its limitations.Moult
Suggestion to remove the limitation of max. 8 array elements: gist.github.com/zett42/7e18b3fa9cd6614ad6e5e4ce13dad156 . Haven't tested performance yet.Fouts
For 8 elements, yours is faster by a factor of about 1.4. I guess the method call overhead is significant for the looped version. gist.github.com/zett42/ec8c6eaaf333001b5f8e5358d6aca563Fouts
Thanks, @Fouts - please see my update. The biggest gain from a single [HashCode]::Combine() call comes if you don't also have to loop over the array for a different reason, such as for case-insensitivity here.Moult
The performance gain of the C# version (CaseInsensitiveArrayEqualityComparer3) is pretty impressive. On my machine, it runs about 10x faster than the PowerShell switch-based version. Let me know what you think about the code. I tried to make it more generic by testing for the implemented interfaces instead of explicit Array type checking.Fouts
@zett42, github.com/PowerShell/PowerShell/issues/17614 may be of interest: when I looked at your C# code, I noticed the new to , and tried to avoid with an explicit interface implementation - only to find that PowerShell then can't find the .Equals() method except with an interface cast.Moult
Love this answer, already had it upvoted, but after some research I'm wondering why didn't you use ([IStructuralEquatable] $o).GetHashCode([StringComparer]::InvariantCultureIgnoreCase) ?Corrie
Thanks, @Santiago. The answer is simple: Because I didn't know about it :) Nice job. Please see my update.Moult
my pleasure ;) IStructuralEquatable is indeed an awesome interfaceCorrie
Have been tinkering a bit with this interface, decided to test it for a function to get unique values from objects and compared with Sort-Object -Unique the results are pretty Ok. And also a class to compare PSCustomObjects. If you see any room for improvements and / or are willing to review them it would be much appreciatedCorrie
Thanks, @Santiago, I'll take a look. Quick note: re Select-Unique: it is actually more similar to Select-Object -UniqueMoult
It is, but Select-Object can do it on one property only while Sort-Object can work on multiple properties.Corrie
For manually hashing I was attempting to replicate this technique used by Jon Skeet in C# but unfortunately we don't have the unchecked statement in pwsh, not sure how or if there is a why it could be replicatedCorrie
@Santiago. Unfortunately, it's both complicated and slow to replicate unchecked behavior in PowerShell, at least to my knowledge. The only way I've found is via hex string representations; e.g.: [int] ('0x' + ([long]::MaxValue.ToString('x') -replace '^.*(?=.{8}$)')) # -> -1. I've created a ConvertTo-IntegerUnchecked function in this Gist that generalizes the technique, though, given the poor performance, it is probably mostly of interest as an educational tool. -Verbose shows what's happening behind the scenes.Moult
Good point, @iRon, thanks. Please see my update, which uses [array] rather than @(...) for efficiency, however (though that may not matter in practice).Moult
F
4

Edit: While the following solution works, it isn't nearly as clean as the solution provided by mklement0, which I recommend to use. Tuples can be useful in many other situations, so I'll keep this answer as an example.


Instead of joining the keys I suggest to use a "split key" by the help of the Tuple class. In this case there is no need for a delimiter, as the keys are not joined but stored as separate properties in an object. The Tuple class provides the necessary interfaces so the tuple acts like a single key when used in any Dictionary (e. g. Hashtable).

$List =  ConvertFrom-Csv @'
Id, LastName, FirstName, Country
 1, Aerts,    Ronald,    Belgium
 2, Berg,     Ashly,     Germany
 3, Cook,     James,     England
 4, Duval,    Frank,     France
 5, Lyberg,   Ash,       England
 6, Fischer,  Adam,      Germany
'@

$Index = @{}
$List.ForEach{ $Index[ [Tuple]::Create( $_.LastName, $_.FirstName ) ] = $_ }

$Index

When written to the console, the split key gets nicely formatted:

Name                           Value
----                           -----
(Berg, Ashly)                  @{Id=2; LastName=Berg; FirstName=Ashly; Country=Germany}
(Lyberg, Ash)                  @{Id=5; LastName=Lyberg; FirstName=Ash; Country=England}
(Duval, Frank)                 @{Id=4; LastName=Duval; FirstName=Frank; Country=France}
(Aerts, Ronald)                @{Id=1; LastName=Aerts; FirstName=Ronald; Country=Belgium}
(Cook, James)                  @{Id=3; LastName=Cook; FirstName=James; Country=England}
(Fischer, Adam)                @{Id=6; LastName=Fischer; FirstName=Adam; Country=Germany}

To look up an entry, create a temporary tuple:

$Index[ [Tuple]::Create('Duval','Frank') ]

An advantage of the Tuple class is that you can easily get the individual keys that make up the split key, without having to split a string:

# Using member access enumeration
$Index.Keys.Item1  # Prints all last names
$Index.Keys.Item2  # Prints all first names

# Using the enumerator to loop over the index
$Index.GetEnumerator().ForEach{ $_.Key.Item1 }

The .NET Framework 4.7 adds the ValueTuple struct (what's the difference?). It might be worth testing whether it gives better performance for this use case. Also, replacing Hashtable by a generic Dictionary could improve performance as well:

$Index = [Collections.Generic.Dictionary[ ValueTuple[String,String], object]]::new()

Apart from construction of the dictionary, ValueTuple can be used like Tuple. Simply replace Tuple by ValueTuple in the previous code samples.

Fouts answered 27/6, 2022 at 14:6 Comment(0)
R
0

For what it is worth, an ArrayEqualityComparer that (optionally) supports MatchCase and StrictMode:

class ArrayEqualityComparer: System.Collections.IEqualityComparer {
    [bool]$MatchCase
    [bool]$Strict
    [bool]Equals($Object1, $Object2) {
        $Array1 = [System.Collections.IStructuralEquatable] [array] $Object1
        $Array2 = [System.Collections.IStructuralEquatable] [array] $Object2
        $Count = $Array1.get_Count()
        if ($Count -ne $Array2.get_Count()) { return $False }
        else {
            for ($i = 0; $i -lt $Count; $i++) {
                if ( $Null -ne $Array1[$i] -and $Null -ne $Array2[$i] ) {
                    if ($This.Strict -and $Array1[$i].GetType() -ne $Array2[$i].GetType()) { return $False }
                    if ($This.MatchCase) { if ($Array1[$i] -cne $Array2[$i]) { return $False } }
                    elseif ($Array1[$i] -ne $Array2[$i]) { return $False }
                } elseif ( $Null -eq $Array1[$i] -or $Null -eq $Array2[$i] ) { return $False }
            }
        }
        return $True
    }
    [int]GetHashCode($Object) {
        [int]$HashCode = 0
        foreach ($Item in $Object) {
            if ($Null -ne $Item) {
                if ($Item -is [String]) { if (!$This.MatchCase) { $Item = $Item.ToLowerInvariant() } }
                elseif (!$This.Strict) { $Item = "$Item" }
                $HashCode = $HashCode -bXor $Item.GetHashCode()
            }
        }
        return $HashCode
    }
    ArrayEqualityComparer() {}
    ArrayEqualityComparer( [bool]$MatchCase ) { $This.MatchCase = $MatchCase }
    ArrayEqualityComparer( [bool]$MatchCase, [bool] $Strict ) { $This.MatchCase = $MatchCase; $This.Strict = $Strict }
}

(Pester) Tests:

Describe 'ArrayEqualityComparer' {

    Context 'Loose, Case Insensitive' {

        BeforeAll {
            $Comparer = [ArrayEqualityComparer]::new()
            $Hashtable = [hashtable]::new($Comparer)
            $Hashtable['abc'] = 1
            $Hashtable[('def', '123')] = 2
            $Hashtable[('ghi', '456', 'jkl')] = 3
        }

        It 'Contains' {
            $Hashtable.Contains('abc')                 |Should -BeTrue
            $Hashtable.Contains(('def', '123'))        |Should -BeTrue
            $Hashtable.Contains(('DEF', '123'))        |Should -BeTrue
            $Hashtable.Contains(('def', 123))          |Should -BeTrue
            $Hashtable.Contains(('DEF', 123))          |Should -BeTrue
            $Hashtable.Contains(('ghi', '456', 'jkl')) |Should -BeTrue
        }

        It 'Value' {
            $Hashtable.'abc'                 |Should -Be 1
            $Hashtable.('def', '123')        |Should -Be 2
            $Hashtable.('DEF', '123')        |Should -Be 2
            $Hashtable.('def', 123)          |Should -Be 2
            $Hashtable.('DEF', 123)          |Should -Be 2
            $Hashtable.('ghi', '456', 'jkl') |Should -Be 3
        }
    }

    Context 'Loose, Case Sensitive' {

        BeforeAll {
            $Comparer = [ArrayEqualityComparer]::new($True)
            $Hashtable = [hashtable]::new($Comparer)
            $Hashtable['abc'] = 1
            $Hashtable[('def', '123')] = 2
            $Hashtable[('ghi', '456', 'jkl')] = 3
        }

        It 'Contains' {
            $Hashtable.Contains('abc')                 |Should -BeTrue
            $Hashtable.Contains(('def', '123'))        |Should -BeTrue
            $Hashtable.Contains(('DEF', '123'))        |Should -BeFalse
            $Hashtable.Contains(('def', 123))          |Should -BeTrue
            $Hashtable.Contains(('DEF', 123))          |Should -BeFalse
            $Hashtable.Contains(('ghi', '456', 'jkl')) |Should -BeTrue
        }

        It 'Value' {
            $Hashtable.'abc'                 |Should -Be 1
            $Hashtable.('def', '123')        |Should -Be 2
            $Hashtable.('DEF', '123')        |Should -BeNullOrEmpty
            $Hashtable.('def', 123)          |Should -Be 2
            $Hashtable.('DEF', 123)          |Should -BeNullOrEmpty
            $Hashtable.('ghi', '456', 'jkl') |Should -Be 3
        }
    }

    Context 'Strict, Case Insensitive' {

        BeforeAll {
            $Comparer = [ArrayEqualityComparer]::new($False, $True)
            $Hashtable = [hashtable]::new($Comparer)
            $Hashtable['abc'] = 1
            $Hashtable[('def', '123')] = 2
            $Hashtable[('ghi', '456', 'jkl')] = 3
        }

        It 'Contains' {
            $Hashtable.Contains('abc')                 |Should -BeTrue
            $Hashtable.Contains(('def', '123'))        |Should -BeTrue
            $Hashtable.Contains(('DEF', '123'))        |Should -BeTrue
            $Hashtable.Contains(('def', 123))          |Should -BeFalse
            $Hashtable.Contains(('DEF', 123))          |Should -BeFalse
            $Hashtable.Contains(('ghi', '456', 'jkl')) |Should -BeTrue
        }

        It 'Value' {
            $Hashtable.'abc' |Should -Be 1
            $Hashtable.('def', '123')        |Should -Be 2
            $Hashtable.('DEF', '123')        |Should -Be 2
            $Hashtable.('def', 123)          |Should -BeNullOrEmpty
            $Hashtable.('DEF', 123)          |Should -BeNullOrEmpty
            $Hashtable.('ghi', '456', 'jkl') |Should -Be 3
        }
    }

    Context 'Strict, Case Sensitive' {

        BeforeAll {
            $Comparer = [ArrayEqualityComparer]::new($True, $True)
            $Hashtable = [hashtable]::new($Comparer)
            $Hashtable['abc'] = 1
            $Hashtable[('def', '123')] = 2
            $Hashtable[('ghi', '456', 'jkl')] = 3
        }

        It 'Contains' {
            $Hashtable.Contains('abc')                 |Should -BeTrue
            $Hashtable.Contains(('def', '123'))        |Should -BeTrue
            $Hashtable.Contains(('DEF', '123'))        |Should -BeFalse
            $Hashtable.Contains(('def', 123))          |Should -BeFalse
            $Hashtable.Contains(('DEF', 123))          |Should -BeFalse
            $Hashtable.Contains(('ghi', '456', 'jkl')) |Should -BeTrue
        }

        It 'Value' {
            $Hashtable.'abc'                 |Should -Be 1
            $Hashtable.('def', '123')        |Should -Be 2
            $Hashtable.('DEF', '123')        |Should -BeNullOrEmpty
            $Hashtable.('def', 123)          |Should -BeNullOrEmpty
            $Hashtable.('DEF', 123)          |Should -BeNullOrEmpty
            $Hashtable.('ghi', '456', 'jkl') |Should -Be 3
        }
    }
}

Unfortunately, it is about 20% slower than join the keys with a delimiter

Ronnieronny answered 29/12, 2022 at 13:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.