diff --git a/R4Utils/ValueEqualityCollections/Extensions.cs b/R4Utils/ValueEqualityCollections/Extensions.cs new file mode 100644 index 0000000..dfb6161 --- /dev/null +++ b/R4Utils/ValueEqualityCollections/Extensions.cs @@ -0,0 +1,21 @@ +namespace R4Utils.ValueEqualityCollections; + +public static class ValueEqualityCollectionsExtensions +{ + /// + /// Transform this collection into a collection that performs value equality on its items for deducing collection equality. + ///

+ /// The resulting will consider ordering whenever compared to another instance also considering ordering. + ///
+ public static ValueEqualityCollection AsOrderedValueEqualityCollection(this IList collection) + where T : IEquatable => new(collection, ValueEqualityCollection.OrderMode.Consider); + + /// + /// Transform this collection into a collection that performs value equality on its items for deducing collection equality. + ///

+ /// As the underlying does not support ordering (does not inherit ), + /// the resulting will ignore ordering whenever it is compared. + ///
+ public static ValueEqualityCollection AsValueEqualityCollection(this ICollection collection) + where T : IEquatable => new(collection, ValueEqualityCollection.OrderMode.Ignore); +} diff --git a/R4Utils/ValueEqualityCollections/ValueEqualityCollection.cs b/R4Utils/ValueEqualityCollections/ValueEqualityCollection.cs new file mode 100644 index 0000000..c7e2a64 --- /dev/null +++ b/R4Utils/ValueEqualityCollections/ValueEqualityCollection.cs @@ -0,0 +1,137 @@ +using System.Collections; +using System.Numerics; + +namespace R4Utils.ValueEqualityCollections; + +/// +/// A wrapper around an that uses value equality of its elements for equality of the collections. +/// +public class ValueEqualityCollection : ICollection, IEquatable>, + IEqualityOperators, ValueEqualityCollection, bool> where T : IEquatable +{ + /// + /// Defines strategies of dealing with ordering when comparing two instances. + /// + public enum OrderMode + { + /// + /// Consider ordering when comparing this instance with another one. + /// + Consider, + + /// + /// Ignore ordering when comparing this instance with another one. + ///

+ /// If any of the compared instances has this set, the ordering will be ignored. + ///
+ Ignore, + } + + private ICollection Collection { get; } + + /// + /// Whether to consider ordering when comparing this instance with another one. + ///

+ /// Regardless of this property, ordering will only be considered if both collections implement . + ///
+ public OrderMode Ordering { get; } + + /// + /// Create a new wrapper around that uses items equality for instance equality. + /// + public ValueEqualityCollection(ICollection collection, OrderMode ordering) + { + Collection = collection; + Ordering = ordering; + } + + public override string ToString() => + $"{nameof(ValueEqualityCollection)}[{nameof(Ordering)}={Ordering}]({Collection})"; + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)Collection).GetEnumerator(); + } + + public static bool operator ==(ValueEqualityCollection? left, ValueEqualityCollection? right) => + left?.Equals(right) ?? right is null; + + public static bool operator !=(ValueEqualityCollection? left, ValueEqualityCollection? right) => + !(left == right); + + public bool Equals(ValueEqualityCollection? other) + { + if (other is null) return false; + return ReferenceEquals(this, other) || EqualityDispatch(other); + } + + private bool EqualityDispatch(ValueEqualityCollection other) + { + if (Ordering is OrderMode.Consider && other.Ordering is OrderMode.Consider && Collection is IList list1 && + other.Collection is IList list2) + { + return OrderedEquality(list1, list2); + } + + return ScrambledEquals(Collection, other.Collection); + } + + private static bool OrderedEquality(IList list1, IList list2) => list1.SequenceEqual(list2); + + // Source: https://stackoverflow.com/a/3670089/13849454 + private static bool ScrambledEquals(ICollection collection1, ICollection collection2) + { + var cnt = new Dictionary(); + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + foreach (var s in collection1) + { + if (!cnt.TryAdd(s, 1)) + { + cnt[s]++; + } + } + + foreach (var s in collection2) + { + if (cnt.TryGetValue(s, out var value)) + { + cnt[s] = --value; + } + else + { + return false; + } + } + + return cnt.Values.All(c => c == 0); + } + + + // ICollection implementation + + public IEnumerator GetEnumerator() => Collection.GetEnumerator(); + + public override bool Equals(object? obj) + { + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((ValueEqualityCollection)obj); + } + + // TODO: Look into this, this might in fact be wrong. + public override int GetHashCode() => Collection.Aggregate(0, HashCode.Combine); + + public void Add(T item) => Collection.Add(item); + + public void Clear() => Collection.Clear(); + + public bool Contains(T item) => Collection.Contains(item); + + public void CopyTo(T[] array, int arrayIndex) => Collection.CopyTo(array, arrayIndex); + + public bool Remove(T item) => Collection.Remove(item); + + public int Count => Collection.Count; + + public bool IsReadOnly => Collection.IsReadOnly; +} diff --git a/R4UtilsTester/ValueEqualityCollections/TestValueEqualityCollection.cs b/R4UtilsTester/ValueEqualityCollections/TestValueEqualityCollection.cs new file mode 100644 index 0000000..c108f99 --- /dev/null +++ b/R4UtilsTester/ValueEqualityCollections/TestValueEqualityCollection.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using R4Utils.ValueEqualityCollections; + +namespace R4UtilsTester.ValueEqualityCollections; + +[TestFixture] +public class TestValueEqualityCollection +{ + private static void AssertEqual(ValueEqualityCollection wrapper1, ValueEqualityCollection wrapper2, + bool unequal = false) where T : IEquatable + { + if (unequal) + { + Assert.AreNotEqual(wrapper1, wrapper2); + Assert.IsFalse(wrapper1.Equals(wrapper2)); + Assert.IsFalse(wrapper1 == wrapper2); + Assert.IsTrue(wrapper1 != wrapper2); + } + else + { + Assert.AreEqual(wrapper1, wrapper2); + Assert.IsTrue(wrapper1.Equals(wrapper2)); + Assert.IsTrue(wrapper1 == wrapper2); + Assert.IsFalse(wrapper1 != wrapper2); + } + } + + [Test] + public void TestEqualArraysEqual() + { + int[] data1 = [1, 2, 3]; + int[] data2 = [1, 2, 3]; + var wrapper1 = data1.AsOrderedValueEqualityCollection(); + var wrapper2 = data2.AsOrderedValueEqualityCollection(); + AssertEqual(wrapper1, wrapper2); + } + + [Test] + public void TestConsidersOrderingOfElements() + { + int[] data1 = [1, 2, 3]; + int[] data2 = [2, 1, 3]; + var wrapper1 = data1.AsOrderedValueEqualityCollection(); + var wrapper2 = data2.AsOrderedValueEqualityCollection(); + AssertEqual(wrapper1, wrapper2, true); + } + + [Test] + public void TestHandlesSetsCorrectly() + { + HashSet data1 = [1, 2, 3]; + HashSet data2 = [2, 1, 3]; + var wrapper1 = data1.AsValueEqualityCollection(); + var wrapper2 = data2.AsValueEqualityCollection(); + AssertEqual(wrapper1, wrapper2); + } + + [Test] + public void TestHandlesSetsMixedWithArraysCorrectly() + { + HashSet data1 = [1, 2, 3]; + int[] data2 = [2, 1, 3]; + var wrapper1 = data1.AsValueEqualityCollection(); + var wrapper2 = data2.AsOrderedValueEqualityCollection(); + AssertEqual(wrapper1, wrapper2); + } + + [Test] + public void TestIgnoreOrdering() + { + bool[] bools = [true, false]; + foreach (var data1IgnoreOrdering in bools) + { + foreach (var data2IgnoreOrdering in bools) + { + int[] data1 = [1, 2, 3]; + int[] data2 = [2, 1, 3]; + var wrapper1 = data1IgnoreOrdering + ? data1.AsValueEqualityCollection() + : data1.AsOrderedValueEqualityCollection(); + var wrapper2 = data2IgnoreOrdering + ? data2.AsValueEqualityCollection() + : data2.AsOrderedValueEqualityCollection(); + Assert.AreEqual(wrapper1.Ordering, + data1IgnoreOrdering + ? ValueEqualityCollection.OrderMode.Ignore + : ValueEqualityCollection.OrderMode.Consider); + Assert.AreEqual(wrapper2.Ordering, + data2IgnoreOrdering + ? ValueEqualityCollection.OrderMode.Ignore + : ValueEqualityCollection.OrderMode.Consider); + if (data1IgnoreOrdering || data2IgnoreOrdering) + { + AssertEqual(wrapper1, wrapper2); + } + else + { + AssertEqual(wrapper1, wrapper2, true); + } + } + } + } + + [Test] + public void TestRecursiveWrapper() + { + HashSet data1 = [1, 2, 3]; + int[] data2 = [2, 1, 3]; + var wrapper1 = data1.AsValueEqualityCollection().AsValueEqualityCollection(); + var wrapper2 = data2.AsOrderedValueEqualityCollection(); + AssertEqual(wrapper1, wrapper2); + } + + [Test] + public void TestDuplicateElements() + { + bool[] bools = [true, false]; + foreach (var data1IgnoreOrdering in bools) + { + foreach (var data2IgnoreOrdering in bools) + { + int[] data1 = [1, 2, 3]; + int[] data2 = [1, 2, 3, 3]; + var wrapper1 = data1IgnoreOrdering + ? data1.AsValueEqualityCollection() + : data1.AsOrderedValueEqualityCollection(); + var wrapper2 = data2IgnoreOrdering + ? data2.AsValueEqualityCollection() + : data2.AsOrderedValueEqualityCollection(); + Assert.AreEqual(wrapper1.Ordering, + data1IgnoreOrdering + ? ValueEqualityCollection.OrderMode.Ignore + : ValueEqualityCollection.OrderMode.Consider); + Assert.AreEqual(wrapper2.Ordering, + data2IgnoreOrdering + ? ValueEqualityCollection.OrderMode.Ignore + : ValueEqualityCollection.OrderMode.Consider); + AssertEqual(wrapper1, wrapper2, true); + } + } + } +}