From 0b0503408bf96329f699b1524a58f231b704ecf8 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Sat, 13 Feb 2021 11:34:01 -0500 Subject: [PATCH 01/11] Update to modern .net core. --- test/Ix.UnitTests/Ix.UnitTests.csproj | 2 +- test/Linq.UnitTests/Linq.UnitTests.csproj | 2 +- test/Rx.UnitTests/Rx.UnitTests.csproj | 2 +- test/UnitTests/UnitTests.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Ix.UnitTests/Ix.UnitTests.csproj b/test/Ix.UnitTests/Ix.UnitTests.csproj index 1d67495..7528432 100644 --- a/test/Ix.UnitTests/Ix.UnitTests.csproj +++ b/test/Ix.UnitTests/Ix.UnitTests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp3.1 diff --git a/test/Linq.UnitTests/Linq.UnitTests.csproj b/test/Linq.UnitTests/Linq.UnitTests.csproj index fb99013..2ee44b7 100644 --- a/test/Linq.UnitTests/Linq.UnitTests.csproj +++ b/test/Linq.UnitTests/Linq.UnitTests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp3.1 diff --git a/test/Rx.UnitTests/Rx.UnitTests.csproj b/test/Rx.UnitTests/Rx.UnitTests.csproj index 8494b76..abdacca 100644 --- a/test/Rx.UnitTests/Rx.UnitTests.csproj +++ b/test/Rx.UnitTests/Rx.UnitTests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp3.1 diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj index 07b624e..23e0583 100644 --- a/test/UnitTests/UnitTests.csproj +++ b/test/UnitTests/UnitTests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp3.1 From b17fea6a269f4d3dd5212f7eddcd4ebf74409d0d Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Sat, 13 Feb 2021 11:34:11 -0500 Subject: [PATCH 02/11] Add commutative hash combiner. --- .../Internals/CommutativeHashCombiner.cs | 43 +++++++++++++++++++ test/UnitTests/Hashes.cs | 21 +++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/Nito.Comparers.Core/Internals/CommutativeHashCombiner.cs diff --git a/src/Nito.Comparers.Core/Internals/CommutativeHashCombiner.cs b/src/Nito.Comparers.Core/Internals/CommutativeHashCombiner.cs new file mode 100644 index 0000000..a892ac6 --- /dev/null +++ b/src/Nito.Comparers.Core/Internals/CommutativeHashCombiner.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Text; + +#pragma warning disable CA1815, CA1721 + +namespace Nito.Comparers.Internals +{ + /// + /// A hash combiner that is implemented with a simple commutative algorithm. This is a mutable struct for performance reasons. + /// + public struct CommutativeHashCombiner + { + private uint _hash; + + /// + /// Gets the current result of the hash function. + /// + public int HashCode => unchecked((int) _hash); + + /// + /// Creates a new hash, starting at . + /// + /// The seed for the hash. Defaults to the FNV hash offset, for no particular reason. + public static CommutativeHashCombiner Create(int seed = unchecked((int)2166136261)) => new() { _hash = unchecked((uint) seed) }; + + /// + /// Adds the specified integer to this hash. This operation is commutative. + /// + /// The integer to hash. + 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; + } + } + } +} diff --git a/test/UnitTests/Hashes.cs b/test/UnitTests/Hashes.cs index 62b159d..3360b63 100644 --- a/test/UnitTests/Hashes.cs +++ b/test/UnitTests/Hashes.cs @@ -25,5 +25,26 @@ public void NullComparerHash_EqualsDefaultMurmer3Hash() var objectHash = comparer.GetHashCode(0); Assert.Equal(Murmur3Hash.Create().HashCode, objectHash); } + + [Fact] + public void CommutativeHashCombiner_IsCommutative() + { + // This unit test assumes the GetHashCode of these two objects are different. + int value1 = 5; + int value2 = 7; + Assert.NotEqual(value1.GetHashCode(), value2.GetHashCode()); + + var hash1 = CommutativeHashCombiner.Create(); + hash1.Combine(value1); + hash1.Combine(value2); + var result1 = hash1.HashCode; + + var hash2 = CommutativeHashCombiner.Create(); + hash2.Combine(value2); + hash2.Combine(value1); + var result2 = hash2.HashCode; + + Assert.Equal(result1, result2); + } } } From 5d445c15c36345e4a94930a303646e1d95e26408 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Sat, 13 Feb 2021 12:12:51 -0500 Subject: [PATCH 03/11] Add unordered sequence equality comparer. --- .../Util/SequenceComparer.cs | 11 +- .../Util/SequenceEqualityComparer.cs | 13 ++- .../Util/UnorderedSequenceEqualityComparer.cs | 101 ++++++++++++++++++ 3 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs diff --git a/src/Nito.Comparers.Core/Util/SequenceComparer.cs b/src/Nito.Comparers.Core/Util/SequenceComparer.cs index fce0205..4030e00 100644 --- a/src/Nito.Comparers.Core/Util/SequenceComparer.cs +++ b/src/Nito.Comparers.Core/Util/SequenceComparer.cs @@ -21,13 +21,10 @@ public SequenceComparer(IComparer? source) /// protected override int DoGetHashCode(IEnumerable obj) { - unchecked - { - var ret = Murmur3Hash.Create(); - foreach (var item in obj) - ret.Combine(SourceGetHashCode(item)); - return ret.HashCode; - } + var ret = Murmur3Hash.Create(); + foreach (var item in obj) + ret.Combine(SourceGetHashCode(item)); + return ret.HashCode; } /// diff --git a/src/Nito.Comparers.Core/Util/SequenceEqualityComparer.cs b/src/Nito.Comparers.Core/Util/SequenceEqualityComparer.cs index fe3d600..56ed126 100644 --- a/src/Nito.Comparers.Core/Util/SequenceEqualityComparer.cs +++ b/src/Nito.Comparers.Core/Util/SequenceEqualityComparer.cs @@ -21,13 +21,10 @@ public SequenceEqualityComparer(IEqualityComparer? source) /// protected override int DoGetHashCode(IEnumerable obj) { - unchecked - { - var ret = Murmur3Hash.Create(); - foreach (var item in obj) - ret.Combine(Source.GetHashCode(item!)); - return ret.HashCode; - } + var ret = Murmur3Hash.Create(); + foreach (var item in obj) + ret.Combine(Source.GetHashCode(item!)); + return ret.HashCode; } /// @@ -41,6 +38,8 @@ protected override bool DoEquals(IEnumerable x, IEnumerable y) { if (xCount.Value != yCount.Value) return false; + if (xCount.Value == 0) + return true; } } diff --git a/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs b/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs new file mode 100644 index 0000000..3cee69d --- /dev/null +++ b/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs @@ -0,0 +1,101 @@ +using Nito.Comparers.Internals; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Nito.Comparers.Util +{ + /// + /// A comparer that performs an unordered equality comparison on a sequence. + /// + /// The type of sequence elements being compared. + internal sealed class UnorderedSequenceEqualityComparer : SourceEqualityComparerBase, T> + { + /// + /// Initializes a new instance of the class. + /// + /// The source comparer. If this is null, the default comparer is used. + public UnorderedSequenceEqualityComparer(IEqualityComparer? source) + : base(source, false) + { + } + + /// + protected override int DoGetHashCode(IEnumerable obj) + { + var ret = CommutativeHashCombiner.Create(); + foreach (var item in obj) + ret.Combine(Source.GetHashCode(item!)); + return ret.HashCode; + } + + /// + protected override bool DoEquals(IEnumerable x, IEnumerable 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(EqualityComparerBuilder.For().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(x => x.Value == 0); + } + + return false; + } + + if (!yIter.MoveNext()) + return false; + + // If both items are equivalent, just skip the equivalence class counts. + var ret = Source.Equals(xIter.Current, yIter.Current); + if (ret) + 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.ContainsKey(xKey)) + ++equivalenceClassCounts[xKey]; + else + equivalenceClassCounts[xKey] = 1; + if (equivalenceClassCounts.ContainsKey(yKey)) + --equivalenceClassCounts[yKey]; + else + equivalenceClassCounts[yKey] = -1; + } + } + } + + /// + /// Returns a short, human-readable description of the comparer. This is intended for debugging and not for other purposes. + /// + public override string ToString() => $"UnorderedSequence<{typeof(T).Name}>({Source})"; + + private struct Wrapper + { + public T Value { get; set; } + } + } +} From 175bf57598cb72cf02760ad598f2a60c00205992 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Sat, 13 Feb 2021 12:35:59 -0500 Subject: [PATCH 04/11] Simplifications. --- .../Util/UnorderedSequenceEqualityComparer.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs b/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs index 3cee69d..061ad11 100644 --- a/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs +++ b/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs @@ -58,7 +58,7 @@ protected override bool DoEquals(IEnumerable x, IEnumerable y) { // 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(x => x.Value == 0); + return equivalenceClassCounts.All(kvp => kvp.Value == 0); } return false; @@ -68,22 +68,23 @@ protected override bool DoEquals(IEnumerable x, IEnumerable y) return false; // If both items are equivalent, just skip the equivalence class counts. - var ret = Source.Equals(xIter.Current, yIter.Current); - if (ret) + 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.ContainsKey(xKey)) - ++equivalenceClassCounts[xKey]; + if (equivalenceClassCounts.TryGetValue(xKey, out var xValue)) + ++xValue; else - equivalenceClassCounts[xKey] = 1; - if (equivalenceClassCounts.ContainsKey(yKey)) - --equivalenceClassCounts[yKey]; + xValue = 1; + equivalenceClassCounts[xKey] = xValue; + if (equivalenceClassCounts.TryGetValue(yKey, out var yValue)) + --yValue; else - equivalenceClassCounts[yKey] = -1; + yValue = -1; + equivalenceClassCounts[yKey] = yValue; } } } From 138c248d42218ff525fc85df65f55a704a48f2cc Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Sat, 13 Feb 2021 12:36:53 -0500 Subject: [PATCH 05/11] Add overload for EquateSequence. --- .../EqualityComparerExtensions.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Nito.Comparers.Core/EqualityComparerExtensions.cs b/src/Nito.Comparers.Core/EqualityComparerExtensions.cs index 1b6f1f0..d2f295a 100644 --- a/src/Nito.Comparers.Core/EqualityComparerExtensions.cs +++ b/src/Nito.Comparers.Core/EqualityComparerExtensions.cs @@ -53,12 +53,22 @@ public static IFullEqualityComparer ThenEquateBy(this IEqualityCompa } /// - /// Returns an equality comparer that will perform a lexicographical ordering on a sequence of items. + /// Returns an equality comparer that will perform a lexicographical equality on a sequence of items. + /// This is the same as calling with ignoreOrder set to false. /// /// The type of sequence elements being compared. /// The source comparer. If this is null, the default comparer is used. - /// A comparer that will perform a lexicographical ordering on a sequence of items. - public static IFullEqualityComparer> EquateSequence(this IEqualityComparer? source) => - new SequenceEqualityComparer(source); + /// A comparer that will perform a lexicographical equality on a sequence of items. + public static IFullEqualityComparer> EquateSequence(this IEqualityComparer? source) => source.EquateSequence(ignoreOrder: false); + + /// + /// Returns an equality comparer that will perform a lexicographical or unordered equality on a sequence of items. + /// + /// The type of sequence elements being compared. + /// The source comparer. If this is null, the default comparer is used. + /// Whether the sequences will be equated ignoring order. + /// A comparer that will perform an equality on a sequence of items. + public static IFullEqualityComparer> EquateSequence(this IEqualityComparer? source, bool ignoreOrder) => + ignoreOrder ? new UnorderedSequenceEqualityComparer(source) : new SequenceEqualityComparer(source); } } From b3ca89e41a3f38e636e1f1369917c2a23f77fee1 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Sat, 13 Feb 2021 12:50:22 -0500 Subject: [PATCH 06/11] Basic unit tests. --- .../EqualityCompare_EquateSequence.cs | 1 - ...ualityCompare_EquateSequenceIgnoreOrder.cs | 274 ++++++++++++++++++ 2 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs diff --git a/test/UnitTests/EqualityCompare_EquateSequence.cs b/test/UnitTests/EqualityCompare_EquateSequence.cs index be87773..c2f68c0 100644 --- a/test/UnitTests/EqualityCompare_EquateSequence.cs +++ b/test/UnitTests/EqualityCompare_EquateSequence.cs @@ -3,7 +3,6 @@ using System.Linq; using Nito.Comparers; using Xunit; -using System.Globalization; namespace UnitTests { diff --git a/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs b/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs new file mode 100644 index 0000000..0eeba28 --- /dev/null +++ b/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nito.Comparers; +using Xunit; + +namespace UnitTests +{ + public class EqualityCompare_EquateSequenceIgnoreOrder + { + [Fact] + public void SubstitutesCompareDefaultForComparerDefault() + { + var comparer = EqualityComparer.Default.EquateSequence(ignoreOrder: true); + Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).ToString(), comparer.ToString()); + } + + [Fact] + public void SubstitutesCompareDefaultForNull() + { + IEqualityComparer source = null; + var comparer = source.EquateSequence(ignoreOrder: true); + Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).ToString(), comparer.ToString()); + } + + [Fact] + public void ShorterSequenceIsNotEqualToLongerSequenceIfElementsAreEqual() + { + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(new[] { 3, 4 }, new[] { 3, 4, 5 })); + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(new[] { 3, 4, 5 }, new[] { 3, 4 })); + } + + [Fact] + public void ShorterEnumerableIsNotEqualToLongerEnumerableIfElementsAreEqual() + { + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(E_3_4(), E_3_4_5())); + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(E_3_4_5(), E_3_4())); + } + + [Fact] + public void ShorterICollectionIsNotEqualToLongerICollectionIfElementsAreEqual() + { + var e34 = new NongenericICollection(E_3_4().ToList()); + var e345 = new NongenericICollection(E_3_4_5().ToList()); + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(e34, e345)); + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(e345, e34)); + } + + [Fact] + public void ShorterGenericICollectionIsNotEqualToLongerGenericICollectionIfElementsAreEqual() + { + var e34 = new GenericICollection(E_3_4().ToList()); + var e345 = new GenericICollection(E_3_4_5().ToList()); + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(e34, e345)); + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(e345, e34)); + } + + private sealed class NongenericICollection : IEnumerable, System.Collections.ICollection + { + private readonly List _source; + + public NongenericICollection(List source) + { + _source = source; + } + + public int Count + { + get + { + return _source.Count; + } + } + + public bool IsSynchronized + { + get + { + return ((System.Collections.ICollection)_source).IsSynchronized; + } + } + + public object SyncRoot + { + get + { + return ((System.Collections.ICollection)_source).SyncRoot; + } + } + + public void CopyTo(Array array, int index) + { + ((System.Collections.ICollection)_source).CopyTo(array, index); + } + + public IEnumerator GetEnumerator() + { + return _source.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return _source.GetEnumerator(); + } + } + + private sealed class GenericICollection : ICollection + { + private readonly ICollection _source; + + public GenericICollection(ICollection source) + { + _source = source; + } + + public int Count + { + get + { + return _source.Count; + } + } + + public bool IsReadOnly + { + get + { + return _source.IsReadOnly; + } + } + + public void Add(T item) + { + _source.Add(item); + } + + public void Clear() + { + _source.Clear(); + } + + public bool Contains(T item) + { + return _source.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + _source.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return _source.GetEnumerator(); + } + + public bool Remove(T item) + { + return _source.Remove(item); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return _source.GetEnumerator(); + } + } + + [Fact] + public void SequencesAreEqualIfElementsAreEqual() + { + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(new[] { 3, 4 }, new[] { 3, 4 })); + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(new[] { 3, 4, 5 }, new[] { 3, 4, 5 })); + Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(new[] { 3, 4 }), EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(new[] { 3, 4 })); + } + + [Fact] + public void SequencesAreEqualIfElementsAreEqualInDifferentOrder() + { + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(new[] { 3, 4 }, new[] { 4, 3 })); + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(new[] { 3, 4, 5 }, new[] { 5, 4, 3 })); + Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(new[] { 3, 4 }), EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(new[] { 4, 3 })); + } + + [Fact] + public void EnumerablesAreEqualIfElementsAreEqual() + { + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(E_3_4(), E_3_4())); + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(E_3_4_5(), E_3_4_5())); + Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(E_3_4()), EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(E_3_4())); + } + + [Fact] + public void EnumerablesAreEqualIfElementsAreEqualInDifferentOrder() + { + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(E_3_4(), E_4_3())); + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(E_3_4_5(), E_5_4_3())); + Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(E_3_4()), EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(E_4_3())); + } + + [Fact] + public void EqualLengthSequencesWithUnequalElementsAreNotEqual() + { + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(new[] { 3, 4 }, new[] { 3, 5 })); + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(new[] { 3, 4, 5 }, new[] { 3, 3, 5 })); + } + + [Fact] + public void EqualLengthEnumerableWithUnequalElementsAreNotEqual() + { + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(E_3_4(), E_3_5())); + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(E_3_4_5(), E_3_3_5())); + } + + [Fact] + public void SequenceUsesSourceComparerForElementComparisons() + { + var comparer = StringComparer.InvariantCultureIgnoreCase.EquateSequence(ignoreOrder: true); + Assert.True(comparer.Equals(new[] { "a", "b" }, new[] { "B", "A" })); + Assert.Equal(comparer.GetHashCode(new[] { "a", "b" }), comparer.GetHashCode(new[] { "B", "A" })); + } + + [Fact] + public void NullIsNotEqualToEmpty() + { + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(null, Enumerable.Empty())); + Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(Enumerable.Empty(), null)); + } + + private static IEnumerable E_3_4() + { + yield return 3; + yield return 4; + } + + private static IEnumerable E_4_3() + { + yield return 4; + yield return 3; + } + + private static IEnumerable E_3_4_5() + { + yield return 3; + yield return 4; + yield return 5; + } + + private static IEnumerable E_5_4_3() + { + yield return 5; + yield return 4; + yield return 3; + } + + private static IEnumerable E_3_5() + { + yield return 3; + yield return 5; + } + + private static IEnumerable E_3_3_5() + { + yield return 3; + yield return 3; + yield return 5; + } + + [Fact] + public void ToString_DumpsComparer() + { + Assert.Equal("UnorderedSequence(Default(Int32: IComparable))", EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).ToString()); + } + } +} From b4a179c5a0d7e708d7f3b821138149111e031403 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Sat, 13 Feb 2021 12:52:42 -0500 Subject: [PATCH 07/11] Add standard behavior unit tests. --- test/UnitTests/IEqualityComparerTUnitTests.cs | 6 ++++++ test/UnitTests/IEqualityComparerUnitTests.cs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/test/UnitTests/IEqualityComparerTUnitTests.cs b/test/UnitTests/IEqualityComparerTUnitTests.cs index eb32114..a71d0f2 100644 --- a/test/UnitTests/IEqualityComparerTUnitTests.cs +++ b/test/UnitTests/IEqualityComparerTUnitTests.cs @@ -66,6 +66,12 @@ public class IEqualityComparerTUnitTests () => EqualityComparerBuilder.For().EquateBy(x => x.Id, (IEqualityComparer)null, false).EquateSequence(), () => EqualityComparerBuilder.For().Default().EquateSequence(), () => EqualityComparerBuilder.For().Default().EquateSequence(), + () => EqualityComparerBuilder.For().Default().EquateSequence(true), + () => EqualityComparerBuilder.For().Default().EquateSequence(true), + () => EqualityComparerBuilder.For().Default().EquateSequence(true), + () => EqualityComparerBuilder.For().EquateBy(x => x.Id, (IEqualityComparer)null, false).EquateSequence(true), + () => EqualityComparerBuilder.For().Default().EquateSequence(true), + () => EqualityComparerBuilder.For().Default().EquateSequence(true), () => ComparerBuilder.For().Default().Sequence(), () => ComparerBuilder.For().Default().Sequence(), () => ComparerBuilder.For().Default().Sequence(), diff --git a/test/UnitTests/IEqualityComparerUnitTests.cs b/test/UnitTests/IEqualityComparerUnitTests.cs index cc57007..9245b4a 100644 --- a/test/UnitTests/IEqualityComparerUnitTests.cs +++ b/test/UnitTests/IEqualityComparerUnitTests.cs @@ -74,6 +74,12 @@ public class IEqualityComparerUnitTests () => EqualityComparerBuilder.For().EquateBy(x => x.Id, (IEqualityComparer)null, false).EquateSequence(), () => EqualityComparerBuilder.For().Default().EquateSequence(), () => EqualityComparerBuilder.For().Default().EquateSequence(), + () => EqualityComparerBuilder.For().Default().EquateSequence(true), + () => EqualityComparerBuilder.For().Default().EquateSequence(true), + () => EqualityComparerBuilder.For().Default().EquateSequence(true), + () => EqualityComparerBuilder.For().EquateBy(x => x.Id, (IEqualityComparer)null, false).EquateSequence(true), + () => EqualityComparerBuilder.For().Default().EquateSequence(true), + () => EqualityComparerBuilder.For().Default().EquateSequence(true), () => ComparerBuilder.For().Default().Sequence(), () => ComparerBuilder.For().Default().Sequence(), () => ComparerBuilder.For().Default().Sequence(), From a510acd653a272dbc23de7586d1eb2ad49082001 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Sat, 13 Feb 2021 13:10:22 -0500 Subject: [PATCH 08/11] Document parameter. --- doc/comparer-extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/comparer-extensions.md b/doc/comparer-extensions.md index 3b820b7..96d3965 100644 --- a/doc/comparer-extensions.md +++ b/doc/comparer-extensions.md @@ -23,7 +23,7 @@ For equality comparers, use `ThenEquateBy`. There is no notion of "descending" f `Sequence` converts an `IComparer` into an `IComparer>` by using lexicographical sorting. -For equality comparers, use `EquateSequence`. +For equality comparers, use `EquateSequence`. `EquateSequence` by default uses lexicographical equality, but it also takes an optional argument to ignore the order of elements when equating the sequence. Ignoring element order does introduce overhead. # Fixing Incomplete Comparers From 59bb1c67dfdafd34085ef0bfd47e8e8343da7572 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Sat, 13 Feb 2021 14:00:13 -0500 Subject: [PATCH 09/11] Add unit tests for storing null as dictionary keys. --- ...EqualityCompare_EquateSequenceIgnoreOrder.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs b/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs index 0eeba28..af94c83 100644 --- a/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs +++ b/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs @@ -219,6 +219,23 @@ public void SequenceUsesSourceComparerForElementComparisons() Assert.Equal(comparer.GetHashCode(new[] { "a", "b" }), comparer.GetHashCode(new[] { "B", "A" })); } + [Fact] + public void SequenceHandlesNull() + { + var comparer = EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true); + Assert.True(comparer.Equals(new int?[] { 3, null }, new int?[] { null, 3 })); + Assert.Equal(comparer.GetHashCode(new int?[] { 3, null }), comparer.GetHashCode(new int?[] { null, 3 })); + } + + [Fact] + public void SequenceHandlesNullInEquivalenceClass() + { + var comparer = EqualityComparerBuilder.For().EquateBy(x => x ?? 0, specialNullHandling: true).EquateSequence(ignoreOrder: true); + Assert.True(comparer.Equals(new int?[] { 3, null }, new int?[] { 0, 3 })); + Assert.True(comparer.Equals(new int?[] { null, 3 }, new int?[] { 3, 0 })); + Assert.Equal(comparer.GetHashCode(new int?[] { 3, null }), comparer.GetHashCode(new int?[] { 0, 3 })); + } + [Fact] public void NullIsNotEqualToEmpty() { From cc726660d055e9cdc957a20923c8b5a1afbcca23 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Sat, 13 Feb 2021 14:03:01 -0500 Subject: [PATCH 10/11] Add unit tests for empty sequences. --- test/UnitTests/EqualityCompare_EquateSequence.cs | 12 ++++++++++++ .../EqualityCompare_EquateSequenceIgnoreOrder.cs | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/test/UnitTests/EqualityCompare_EquateSequence.cs b/test/UnitTests/EqualityCompare_EquateSequence.cs index c2f68c0..270d828 100644 --- a/test/UnitTests/EqualityCompare_EquateSequence.cs +++ b/test/UnitTests/EqualityCompare_EquateSequence.cs @@ -173,6 +173,13 @@ public void SequencesAreEqualIfElementsAreEqual() Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence().GetHashCode(new[] { 3, 4 }), EqualityComparerBuilder.For().Default().EquateSequence().GetHashCode(new[] { 3, 4 })); } + [Fact] + public void EnumerablesAreEqualIfEmpty() + { + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(E_Empty(), E_Empty())); + Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(E_Empty()), EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(E_Empty())); + } + [Fact] public void EnumerablesAreEqualIfElementsAreEqual() { @@ -210,6 +217,11 @@ public void NullIsNotEqualToEmpty() Assert.False(EqualityComparerBuilder.For().Default().EquateSequence().Equals(Enumerable.Empty(), null)); } + private static IEnumerable E_Empty() + { + yield break; + } + private static IEnumerable E_3_4() { yield return 3; diff --git a/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs b/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs index af94c83..f92687c 100644 --- a/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs +++ b/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs @@ -181,6 +181,13 @@ public void SequencesAreEqualIfElementsAreEqualInDifferentOrder() Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(new[] { 3, 4 }), EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(new[] { 4, 3 })); } + [Fact] + public void EnumerablesAreEqualIfEmpty() + { + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(E_Empty(), E_Empty())); + Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(E_Empty()), EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(E_Empty())); + } + [Fact] public void EnumerablesAreEqualIfElementsAreEqual() { @@ -243,6 +250,11 @@ public void NullIsNotEqualToEmpty() Assert.False(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(Enumerable.Empty(), null)); } + private static IEnumerable E_Empty() + { + yield break; + } + private static IEnumerable E_3_4() { yield return 3; From cf909b89bbf5b96edd894dfd0287bf104775e05a Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Sat, 13 Feb 2021 14:08:38 -0500 Subject: [PATCH 11/11] Fix unit tests to test the right things. --- test/UnitTests/EqualityCompare_EquateSequence.cs | 11 +++++++++-- .../EqualityCompare_EquateSequenceIgnoreOrder.cs | 7 +++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/test/UnitTests/EqualityCompare_EquateSequence.cs b/test/UnitTests/EqualityCompare_EquateSequence.cs index 270d828..5f97958 100644 --- a/test/UnitTests/EqualityCompare_EquateSequence.cs +++ b/test/UnitTests/EqualityCompare_EquateSequence.cs @@ -173,11 +173,18 @@ public void SequencesAreEqualIfElementsAreEqual() Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence().GetHashCode(new[] { 3, 4 }), EqualityComparerBuilder.For().Default().EquateSequence().GetHashCode(new[] { 3, 4 })); } + [Fact] + public void SequencesAreEqualIfEmpty() + { + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence().Equals(Array.Empty(), Array.Empty())); + Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence().GetHashCode(Array.Empty()), EqualityComparerBuilder.For().Default().EquateSequence().GetHashCode(Array.Empty())); + } + [Fact] public void EnumerablesAreEqualIfEmpty() { - Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(E_Empty(), E_Empty())); - Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(E_Empty()), EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(E_Empty())); + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence().Equals(E_Empty(), E_Empty())); + Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence().GetHashCode(E_Empty()), EqualityComparerBuilder.For().Default().EquateSequence().GetHashCode(E_Empty())); } [Fact] diff --git a/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs b/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs index f92687c..4ea4520 100644 --- a/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs +++ b/test/UnitTests/EqualityCompare_EquateSequenceIgnoreOrder.cs @@ -180,6 +180,13 @@ public void SequencesAreEqualIfElementsAreEqualInDifferentOrder() Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(new[] { 3, 4, 5 }, new[] { 5, 4, 3 })); Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(new[] { 3, 4 }), EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(new[] { 4, 3 })); } + + [Fact] + public void SequencesAreEqualIfEmpty() + { + Assert.True(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).Equals(Array.Empty(), Array.Empty())); + Assert.Equal(EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(Array.Empty()), EqualityComparerBuilder.For().Default().EquateSequence(ignoreOrder: true).GetHashCode(Array.Empty())); + } [Fact] public void EnumerablesAreEqualIfEmpty()