-
Notifications
You must be signed in to change notification settings - Fork 5
/
LookupComparer.cs
91 lines (77 loc) · 3.3 KB
/
LookupComparer.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
using System.Diagnostics.CodeAnalysis;
namespace Architect.DomainModeling.Comparisons;
/// <summary>
/// <para>
/// Structurally compares <see cref="ILookup{TKey, TElement}"/> objects.
/// </para>
/// </summary>
public static class LookupComparer
{
/// <summary>
/// <para>
/// Returns a hash code over some of the content of the given <paramref name="instance"/>.
/// </para>
/// </summary>
public static int GetLookupHashCode<TKey, TElement>([AllowNull] ILookup<TKey, TElement> instance)
{
// Unfortunately, we can do no better than distinguish between null, empty, and non-empty
// Example:
// Left instance contains keys { "A", "a" } and has StringComparer.Ordinal
// Right instance contains keys { "A" } and has StringComparer.OrdinalIgnoreCase
// Both have the same keys
// Each considers the other equal, because every query on each results in the same result
// (Enumeration results in different results, but enumeration does not count, as such types have no ordering guarantees)
// Equal objects MUST produce equal hash codes
// But without knowledge of each other, there is no reliable way for the two to produce the same hash code
if (instance is null) return 0;
if (instance.Count == 0) return 1;
return 2;
}
/// <summary>
/// <para>
/// Compares the given <see cref="ILookup{TKey, TElement}"/> objects for equality by comparing their elements.
/// </para>
/// </summary>
public static bool LookupEquals<TKey, TElement>([AllowNull] ILookup<TKey, TElement> left, [AllowNull] ILookup<TKey, TElement> right)
{
if (ReferenceEquals(left, right)) return true;
if (left is null || right is null) return false; // Double nulls are already handled above
// The lookups must be equal from the perspective of each
return LeftLeadingEquals(left, right) && LeftLeadingEquals(right, left);
// Local function that compares two lookups from the perspective of the left one
static bool LeftLeadingEquals(ILookup<TKey, TElement> left, ILookup<TKey, TElement> right)
{
foreach (var leftGroup in left)
{
var rightGroup = right[leftGroup.Key];
// Fast path
if (leftGroup is IList<TElement> leftList && rightGroup is IList<TElement> rightList)
{
var leftListCount = leftList.Count;
if (leftListCount != rightList.Count) return false;
// EqualityComparer<T>.Default helps avoid an IEquatable<T> constraint yet still gets optimized: https://github.com/dotnet/coreclr/pull/14125
for (var i = 0; i < leftListCount; i++)
if (!EqualityComparer<TElement>.Default.Equals(leftList[i], rightList[i]))
return false;
}
// Slow path
else
{
if (!ElementEnumerableEquals(leftGroup, rightGroup))
return false;
}
}
return true;
}
// Local function that compares two groups of elements by enumerating them
static bool ElementEnumerableEquals(IEnumerable<TElement> leftGroup, IEnumerable<TElement> rightGroup)
{
using var rightGroupEnumerator = rightGroup.GetEnumerator();
foreach (var leftElement in leftGroup)
if (!rightGroupEnumerator.MoveNext() || !EqualityComparer<TElement>.Default.Equals(leftElement, rightGroupEnumerator.Current))
return false;
if (rightGroupEnumerator.MoveNext()) return false;
return true;
}
}
}