-
-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #36 from StephenCleary/unordered-collection-comparer
Add unordered collection comparer
- Loading branch information
Showing
15 changed files
with
536 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
src/Nito.Comparers.Core/Internals/CommutativeHashCombiner.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
|
||
#pragma warning disable CA1815, CA1721 | ||
|
||
namespace Nito.Comparers.Internals | ||
{ | ||
/// <summary> | ||
/// A hash combiner that is implemented with a simple commutative algorithm. This is a mutable struct for performance reasons. | ||
/// </summary> | ||
public struct CommutativeHashCombiner | ||
{ | ||
private uint _hash; | ||
|
||
/// <summary> | ||
/// Gets the current result of the hash function. | ||
/// </summary> | ||
public int HashCode => unchecked((int) _hash); | ||
|
||
/// <summary> | ||
/// Creates a new hash, starting at <paramref name="seed"/>. | ||
/// </summary> | ||
/// <param name="seed">The seed for the hash. Defaults to the FNV hash offset, for no particular reason.</param> | ||
public static CommutativeHashCombiner Create(int seed = unchecked((int)2166136261)) => new() { _hash = unchecked((uint) seed) }; | ||
|
||
/// <summary> | ||
/// Adds the specified integer to this hash. This operation is commutative. | ||
/// </summary> | ||
/// <param name="data">The integer to hash.</param> | ||
public void Combine(int data) | ||
{ | ||
unchecked | ||
{ | ||
// Simple addition is pretty much the best we can do since this operation must be commutative. | ||
// We also add a constant value to act as a kind of "length counter" in the higher 16 bits. | ||
// The hash combination is free to overflow into the "length counter". | ||
// The higher 16 bits were chosen because that gives a decent distinction for sequences of <64k items while also distinguishing between small integer values (commonly used as ids). | ||
_hash += (uint)data + 65536; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
using Nito.Comparers.Internals; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace Nito.Comparers.Util | ||
{ | ||
/// <summary> | ||
/// A comparer that performs an unordered equality comparison on a sequence. | ||
/// </summary> | ||
/// <typeparam name="T">The type of sequence elements being compared.</typeparam> | ||
internal sealed class UnorderedSequenceEqualityComparer<T> : SourceEqualityComparerBase<IEnumerable<T>, T> | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="UnorderedSequenceEqualityComparer<T>"/> class. | ||
/// </summary> | ||
/// <param name="source">The source comparer. If this is <c>null</c>, the default comparer is used.</param> | ||
public UnorderedSequenceEqualityComparer(IEqualityComparer<T>? source) | ||
: base(source, false) | ||
{ | ||
} | ||
|
||
/// <inheritdoc /> | ||
protected override int DoGetHashCode(IEnumerable<T> obj) | ||
{ | ||
var ret = CommutativeHashCombiner.Create(); | ||
foreach (var item in obj) | ||
ret.Combine(Source.GetHashCode(item!)); | ||
return ret.HashCode; | ||
} | ||
|
||
/// <inheritdoc /> | ||
protected override bool DoEquals(IEnumerable<T> x, IEnumerable<T> y) | ||
{ | ||
var xCount = x.TryGetCount(); | ||
if (xCount != null) | ||
{ | ||
var yCount = y.TryGetCount(); | ||
if (yCount != null) | ||
{ | ||
if (xCount.Value != yCount.Value) | ||
return false; | ||
if (xCount.Value == 0) | ||
return true; | ||
} | ||
} | ||
|
||
var equivalenceClassCounts = new Dictionary<Wrapper, int>(EqualityComparerBuilder.For<Wrapper>().EquateBy(w => w.Value, Source)); | ||
|
||
using (var xIter = x.GetEnumerator()) | ||
using (var yIter = y.GetEnumerator()) | ||
{ | ||
while (true) | ||
{ | ||
if (!xIter.MoveNext()) | ||
{ | ||
if (!yIter.MoveNext()) | ||
{ | ||
// We have reached the end of both sequences simultaneously. | ||
// They are equivalent if all equivalence class counts have canceled each other out. | ||
return equivalenceClassCounts.All(kvp => kvp.Value == 0); | ||
} | ||
|
||
return false; | ||
} | ||
|
||
if (!yIter.MoveNext()) | ||
return false; | ||
|
||
// If both items are equivalent, just skip the equivalence class counts. | ||
if (Source.Equals(xIter.Current, yIter.Current)) | ||
continue; | ||
|
||
var xKey = new Wrapper { Value = xIter.Current }; | ||
var yKey = new Wrapper { Value = yIter.Current }; | ||
|
||
// Treat `x` as adding counts and `y` as subtracting counts; any counts not present are 0. | ||
if (equivalenceClassCounts.TryGetValue(xKey, out var xValue)) | ||
++xValue; | ||
else | ||
xValue = 1; | ||
equivalenceClassCounts[xKey] = xValue; | ||
if (equivalenceClassCounts.TryGetValue(yKey, out var yValue)) | ||
--yValue; | ||
else | ||
yValue = -1; | ||
equivalenceClassCounts[yKey] = yValue; | ||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Returns a short, human-readable description of the comparer. This is intended for debugging and not for other purposes. | ||
/// </summary> | ||
public override string ToString() => $"UnorderedSequence<{typeof(T).Name}>({Source})"; | ||
|
||
private struct Wrapper | ||
{ | ||
public T Value { get; set; } | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.