Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions R4Utils/ValueEqualityCollections/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace R4Utils.ValueEqualityCollections;

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

/// <summary>
/// Transform this collection into a collection that performs value equality on its items for deducing collection equality.
/// <br/><br/>
/// As the underlying <paramref name="collection"/> does not support ordering (does not inherit <see cref="IList{T}"/>),
/// the resulting <see cref="ValueEqualityCollection{T}"/> will ignore ordering whenever it is compared.
/// </summary>
public static ValueEqualityCollection<T> AsValueEqualityCollection<T>(this ICollection<T> collection)
where T : IEquatable<T> => new(collection, ValueEqualityCollection<T>.OrderMode.Ignore);
}
137 changes: 137 additions & 0 deletions R4Utils/ValueEqualityCollections/ValueEqualityCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using System.Collections;
using System.Numerics;

namespace R4Utils.ValueEqualityCollections;

/// <summary>
/// A wrapper around an <see cref="ICollection{T}"/> that uses value equality of its elements for equality of the collections.
/// </summary>
public class ValueEqualityCollection<T> : ICollection<T>, IEquatable<ValueEqualityCollection<T>>,
IEqualityOperators<ValueEqualityCollection<T>, ValueEqualityCollection<T>, bool> where T : IEquatable<T>
{
/// <summary>
/// Defines strategies of dealing with ordering when comparing two instances.
/// </summary>
public enum OrderMode
{
/// <summary>
/// Consider ordering when comparing this instance with another one.
/// </summary>
Consider,

/// <summary>
/// Ignore ordering when comparing this instance with another one.
/// <br/><br/>
/// If any of the compared instances has this set, the ordering will be ignored.
/// </summary>
Ignore,
}

private ICollection<T> Collection { get; }

/// <summary>
/// Whether to consider ordering when comparing this instance with another one.
/// <br/><br/>
/// Regardless of this property, ordering will only be considered if both collections implement <see cref="IList{T}"/>.
/// </summary>
public OrderMode Ordering { get; }

/// <summary>
/// Create a new wrapper around <paramref name="collection"/> that uses items equality for instance equality.
/// </summary>
public ValueEqualityCollection(ICollection<T> collection, OrderMode ordering)
{
Collection = collection;
Ordering = ordering;
}

public override string ToString() =>
$"{nameof(ValueEqualityCollection<T>)}[{nameof(Ordering)}={Ordering}]({Collection})";

IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)Collection).GetEnumerator();
}

public static bool operator ==(ValueEqualityCollection<T>? left, ValueEqualityCollection<T>? right) =>
left?.Equals(right) ?? right is null;

public static bool operator !=(ValueEqualityCollection<T>? left, ValueEqualityCollection<T>? right) =>
!(left == right);

public bool Equals(ValueEqualityCollection<T>? other)
{
if (other is null) return false;
return ReferenceEquals(this, other) || EqualityDispatch(other);
}

private bool EqualityDispatch(ValueEqualityCollection<T> other)
{
if (Ordering is OrderMode.Consider && other.Ordering is OrderMode.Consider && Collection is IList<T> list1 &&
other.Collection is IList<T> list2)
{
return OrderedEquality(list1, list2);
}

return ScrambledEquals(Collection, other.Collection);
}

private static bool OrderedEquality(IList<T> list1, IList<T> list2) => list1.SequenceEqual(list2);

// Source: https://stackoverflow.com/a/3670089/13849454
private static bool ScrambledEquals(ICollection<T> collection1, ICollection<T> collection2)
{
var cnt = new Dictionary<T, int>();
// 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<T> implementation

public IEnumerator<T> 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<T>)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;
}
144 changes: 144 additions & 0 deletions R4UtilsTester/ValueEqualityCollections/TestValueEqualityCollection.cs
Original file line number Diff line number Diff line change
@@ -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<T>(ValueEqualityCollection<T> wrapper1, ValueEqualityCollection<T> wrapper2,
bool unequal = false) where T : IEquatable<T>
{
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<int> data1 = [1, 2, 3];
HashSet<int> data2 = [2, 1, 3];
var wrapper1 = data1.AsValueEqualityCollection();
var wrapper2 = data2.AsValueEqualityCollection();
AssertEqual(wrapper1, wrapper2);
}

[Test]
public void TestHandlesSetsMixedWithArraysCorrectly()
{
HashSet<int> 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<int>.OrderMode.Ignore
: ValueEqualityCollection<int>.OrderMode.Consider);
Assert.AreEqual(wrapper2.Ordering,
data2IgnoreOrdering
? ValueEqualityCollection<int>.OrderMode.Ignore
: ValueEqualityCollection<int>.OrderMode.Consider);
if (data1IgnoreOrdering || data2IgnoreOrdering)
{
AssertEqual(wrapper1, wrapper2);
}
else
{
AssertEqual(wrapper1, wrapper2, true);
}
}
}
}

[Test]
public void TestRecursiveWrapper()
{
HashSet<int> 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<int>.OrderMode.Ignore
: ValueEqualityCollection<int>.OrderMode.Consider);
Assert.AreEqual(wrapper2.Ordering,
data2IgnoreOrdering
? ValueEqualityCollection<int>.OrderMode.Ignore
: ValueEqualityCollection<int>.OrderMode.Consider);
AssertEqual(wrapper1, wrapper2, true);
}
}
}
}