0

I have been using [Linq.Enumerable]::SequenceEqual() to compare the order of items in two arrays, for example

$validOrder = @('one', 'two', 'three')
$provided = @('one', 'two', 'three')
[Linq.Enumerable]::SequenceEqual($validOrder, $provided)

This works but now I realize I want to address capitalization errors independently, so I want to test for order in a case insensitive way. I found this that documents a different method signature, with IEqualityComparer<T> as the third value. That sure seems like the right direction, but I haven't found anything that helps me implement this in powershell. I tried just using 'OrdinalIgnoreCase' for the last argument, which works for [String].FindIndex() as was pointed out in another thread. But alas not here. I also found this which actually makes a custom comparer for a different object type, but that seems like I am just then implementing manually the thing that I actually want, and I am not sure what the value would be of using [Linq.Enumerable]::SequenceEqual(), I could just pass my arrays to my class method and do the work there directly.

I have also made this approach work if (-not (Compare-Object -SyncWindow 0 $validOrder $provided)) { $result = 'ordered' } else { $result = 'disordered' } And it is already case insensitive. But it is also slower, and I may have a lot of these tests to do, so speed will be of benefit.

Lastly I see that this seems to work, and is SUPER simple, case insensitive (if I want) and seemingly fast. The total number of items in the array will always be small, it is the number of repeats with different array that is the performance issue.

$result = ($provided -join ' ') -eq ($validOrder -join ' ')

So, is this last option viable, or am I missing something obvious that argues against it?

Also, I feel like I will have other situations where IEqualityComparer<T> is a useful argument, so knowing how to do it would be useful. Assuming I am reading this right and IEqualityComparer<T> provides a mechanism for a different form of comparison, other than just rolling my own comparison.

Gordon
  • 6,257
  • 6
  • 36
  • 89
  • Counter-example for your ```join``` sample code: ```$validOrder = @('one', 'two', 'three') $provided = @('one two three')``` -> ```$true``` – mclayton Mar 16 '21 at 19:45

2 Answers2

7

I tried just using 'OrdinalIgnoreCase' for the last argument, ...

You're so close:

$lower = 'a b c'.Split()
$upper = 'A B C'.Split()
$ignoreCaseComparer = [System.StringComparer]::OrdinalIgnoreCase

[Linq.Enumerable]::SequenceEqual($lower, $upper, $ignoreCaseComparer)

The StringComparer class - NOT to be confused with the StringComparison enum type - implements IEqualityComparer<string>, and so satisfies the type constraint.

Please note that the above method invocation only works because I used String.Split() to create the method arguments, and Split() explicitly returns a [string[]] - for most other array-like expressions you may find yourself needing to explicitly type the input arguments in order for PowerShell to pass the correct type parameters:

# This will fail - type of `@()` is [object[]], 
# and `SequenceEquals<object>()` doesn't accept stringcomparer
$lower = @(-split 'a b c')
$upper = @(-split 'A B C')
$ignoreCaseComparer = [System.StringComparer]::OrdinalIgnoreCase

[Linq.Enumerable]::SequenceEqual($lower, $upper, $ignoreCaseComparer)
# This will succeed thanks to the cast(s) to [string[]] at the call site
$lower = @(-split 'a b c')
$upper = @(-split 'A B C')
$ignoreCaseComparer = [System.StringComparer]::OrdinalIgnoreCase

[Linq.Enumerable]::SequenceEqual([string[]]$lower, [string[]]$upper, $ignoreCaseComparer)
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • I don't know whether to cry that I was so close, or be pretty happy I got so close on my own. As for the types, I will need to discreetly cast to `[String[]]`, since my actual data is all `[Collections.Generic.List[String]]`. And I suspect I'll need that static trick again. :) – Gordon Mar 16 '21 at 20:05
  • @Gordon In that case you _don't_ need to cast to `[string[]]` - `[List[string]]` implements `[IEnumerable[string]]` :-) – Mathias R. Jessen Mar 16 '21 at 20:30
3

Probably not the best answer to your exact question, but left here for posterity...

If you want to use your own custom comparer (e.g. reversing values from one array before comparing) you can so something like this:

$expected = @("one", "two", "three");
$actual   = @("eno", "owt", "eerht");

# see https://stackoverflow.com/a/32635196/3156906
class ReversingComparer : System.Collections.Generic.IEqualityComparer[string] {
  [bool]Equals([string]$x, [string]$y) { return $x -eq ($y[-1..-$y.Length] -join ''); }
  [int]GetHashCode([string] $x) { return $x.GetHashCode(); }
}

[ReversingComparer] $comparer = [ReversingComparer]::new();

[Linq.Enumerable]::SequenceEqual([string[]]$expected, [string[]]$actual, $comparer);
# true
mclayton
  • 8,025
  • 2
  • 21
  • 26