Skip to content
Open
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
6 changes: 6 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="MinVer" Version="7.0.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.7" />
<PackageVersion Include="FsCheck" Version="3.3.3" />
<PackageVersion Include="FsCheck.Xunit.v3" Version="3.3.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="MessagePack.Annotations" Version="2.5.108" />
<PackageVersion Include="Verify.XunitV3" Version="31.16.3" />
<PackageVersion Include="xunit.v3.mtp-v2" Version="3.2.2" />
</ItemGroup>
Expand Down
17 changes: 9 additions & 8 deletions src/Equatable.Comparers/DictionaryEqualityComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,16 @@ public int GetHashCode(IDictionary<TKey, TValue> obj)
if (obj == null)
return 0;

var hash = new HashCode();
int hashCode = 0;

// sort by key to ensure dictionary with different order are the same
foreach (var pair in obj.OrderBy(d => d.Key))
{
hash.Add(pair.Key, KeyComparer);
hash.Add(pair.Value, ValueComparer);
}
// Commutative SUM ensures hash is insertion-order independent, consistent with
// Equals which uses TryGetValue (also order-independent). Previously GetHashCode
// used OrderBy + sequential HashCode.Add, which was order-dependent and violated
// the contract: two equal dictionaries (same keys/values, different insertion order)
// would produce different hash codes.
foreach (var pair in obj)
hashCode += HashCode.Combine(KeyComparer.GetHashCode(pair.Key!), ValueComparer.GetHashCode(pair.Value!));

return hash.ToHashCode();
return hashCode;
}
}
14 changes: 9 additions & 5 deletions src/Equatable.Comparers/HashSetEqualityComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,16 @@ public int GetHashCode(IEnumerable<TValue> obj)
if (obj == null)
return 0;

var hashCode = new HashCode();
int hashCode = 0;

// sort to ensure set with different order are the same
foreach (var item in obj.OrderBy(s => s))
hashCode.Add(item, Comparer);
// Commutative SUM ensures hash is iteration-order independent, consistent with
// Equals which uses SetEquals (also order-independent). Previously GetHashCode
// used OrderBy + sequential HashCode.Add, which was order-dependent and violated
// the contract: two equal sets (same elements, different insertion order) could
// produce different hash codes.
foreach (var item in obj)
hashCode += Comparer.GetHashCode(item!);

return hashCode.ToHashCode();
return hashCode;
}
}
78 changes: 78 additions & 0 deletions src/Equatable.Comparers/MultiDimensionalArrayEqualityComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
namespace Equatable.Comparers;

/// <summary>
/// Structural equality comparer for multi-dimensional arrays (T[,], T[,,], etc.).
/// Compares element-by-element in row-major order without LINQ or intermediate allocations.
/// </summary>
/// <typeparam name="TValue">The element type of the array.</typeparam>
public class MultiDimensionalArrayEqualityComparer<TValue> : IEqualityComparer<Array?>
{
/// <summary>
/// Gets the default equality comparer for the specified element type.
/// </summary>
public static MultiDimensionalArrayEqualityComparer<TValue> Default { get; } = new();

/// <summary>
/// Initializes a new instance using the default element comparer.
/// </summary>
public MultiDimensionalArrayEqualityComparer() : this(EqualityComparer<TValue>.Default)
{
}

/// <summary>
/// Initializes a new instance using the specified element comparer.
/// </summary>
public MultiDimensionalArrayEqualityComparer(IEqualityComparer<TValue> comparer)
{
Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
}

/// <summary>
/// Gets the element comparer.
/// </summary>
public IEqualityComparer<TValue> Comparer { get; }

/// <inheritdoc />
public bool Equals(Array? x, Array? y)
{
if (ReferenceEquals(x, y))
return true;

if (x is null || y is null)
return false;

if (x.Rank != y.Rank)
return false;

for (int dim = 0; dim < x.Rank; dim++)
{
if (x.GetLength(dim) != y.GetLength(dim))
return false;
}

var ex = x.GetEnumerator();
var ey = y.GetEnumerator();
while (ex.MoveNext())
{
ey.MoveNext();
if (!Comparer.Equals((TValue)ex.Current!, (TValue)ey.Current!))
return false;
}

return true;
}

/// <inheritdoc />
public int GetHashCode(Array? obj)
{
if (obj is null)
return 0;

var hashCode = new HashCode();
var e = obj.GetEnumerator();
while (e.MoveNext())
hashCode.Add((TValue)e.Current!, Comparer);

return hashCode.ToHashCode();
}
}
88 changes: 88 additions & 0 deletions src/Equatable.Comparers/OrderedDictionaryEqualityComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
namespace Equatable.Comparers;

/// <summary>
/// Order-sensitive <see cref="IDictionary{TKey, TValue}"/> equality comparer.
/// Two dictionaries are equal only if they contain the same key/value pairs in the same key-sorted order.
/// </summary>
public class OrderedDictionaryEqualityComparer<TKey, TValue> : IEqualityComparer<IDictionary<TKey, TValue>>
{
/// <summary>Gets the default equality comparer for the specified generic arguments.</summary>
public static OrderedDictionaryEqualityComparer<TKey, TValue> Default { get; } = new();

public OrderedDictionaryEqualityComparer() : this(EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default)
{
}

public OrderedDictionaryEqualityComparer(IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
{
KeyComparer = keyComparer ?? throw new ArgumentNullException(nameof(keyComparer));
ValueComparer = valueComparer ?? throw new ArgumentNullException(nameof(valueComparer));
// Prefer IComparer<TKey> from keyComparer for sort; fall back to a hash-tiebreaker
// to guarantee strict total order (dictionary keys are unique, so ties indicate
// a sort comparer inconsistent with key equality — hash tiebreaker fixes this).
KeySortComparer = keyComparer as IComparer<TKey>
?? new HashTiebreakerComparer(keyComparer);
}

public IEqualityComparer<TKey> KeyComparer { get; }
public IEqualityComparer<TValue> ValueComparer { get; }
private IComparer<TKey> KeySortComparer { get; }

/// <inheritdoc />
public bool Equals(IDictionary<TKey, TValue>? x, IDictionary<TKey, TValue>? y)
{
if (ReferenceEquals(x, y))
return true;

if (x is null || y is null)
return false;

return x.OrderBy(p => p.Key, KeySortComparer).SequenceEqual(y.OrderBy(p => p.Key, KeySortComparer), PairComparer);
}

/// <inheritdoc />
public int GetHashCode(IDictionary<TKey, TValue> obj)
{
if (obj == null)
return 0;

var hashCode = new HashCode();

foreach (var pair in obj.OrderBy(p => p.Key, KeySortComparer))
{
hashCode.Add(pair.Key, KeyComparer);
hashCode.Add(pair.Value, ValueComparer);
}

return hashCode.ToHashCode();
}

private KeyValuePairEqualityComparer PairComparer => new(KeyComparer, ValueComparer);

private sealed class KeyValuePairEqualityComparer(IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
: IEqualityComparer<KeyValuePair<TKey, TValue>>
{
public bool Equals(KeyValuePair<TKey, TValue> x, KeyValuePair<TKey, TValue> y) =>
keyComparer.Equals(x.Key, y.Key) && valueComparer.Equals(x.Value, y.Value);

public int GetHashCode(KeyValuePair<TKey, TValue> obj) =>
HashCode.Combine(keyComparer.GetHashCode(obj.Key!), valueComparer.GetHashCode(obj.Value!));
}

// Provides a strict total order for keys that have no natural IComparer<TKey>,
// using hash code as tiebreaker when the natural comparer returns 0 for distinct keys.
private sealed class HashTiebreakerComparer(IEqualityComparer<TKey> equalityComparer) : IComparer<TKey>
{
private static readonly IComparer<TKey> _natural = Comparer<TKey>.Default;

public int Compare(TKey? x, TKey? y)
{
int cmp = _natural.Compare(x, y);
if (cmp != 0) return cmp;
// Natural comparer considers them equal; break tie by hash code
int hx = x is null ? 0 : equalityComparer.GetHashCode(x);
int hy = y is null ? 0 : equalityComparer.GetHashCode(y);
return hx.CompareTo(hy);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
namespace Equatable.Comparers;

/// <summary>
/// Order-sensitive <see cref="IReadOnlyDictionary{TKey, TValue}"/> equality comparer.
/// Two dictionaries are equal only if they contain the same key/value pairs in the same key-sorted order.
/// </summary>
public class OrderedReadOnlyDictionaryEqualityComparer<TKey, TValue> : IEqualityComparer<IReadOnlyDictionary<TKey, TValue>>
{
/// <summary>Gets the default equality comparer for the specified generic arguments.</summary>
public static OrderedReadOnlyDictionaryEqualityComparer<TKey, TValue> Default { get; } = new();

public OrderedReadOnlyDictionaryEqualityComparer() : this(EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default)
{
}

public OrderedReadOnlyDictionaryEqualityComparer(IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
{
KeyComparer = keyComparer ?? throw new ArgumentNullException(nameof(keyComparer));
ValueComparer = valueComparer ?? throw new ArgumentNullException(nameof(valueComparer));
// Prefer IComparer<TKey> from keyComparer for sort; fall back to a hash-tiebreaker
// to guarantee strict total order (dictionary keys are unique, so ties indicate
// a sort comparer inconsistent with key equality — hash tiebreaker fixes this).
KeySortComparer = keyComparer as IComparer<TKey>
?? new HashTiebreakerComparer(keyComparer);
}

public IEqualityComparer<TKey> KeyComparer { get; }
public IEqualityComparer<TValue> ValueComparer { get; }
private IComparer<TKey> KeySortComparer { get; }

/// <inheritdoc />
public bool Equals(IReadOnlyDictionary<TKey, TValue>? x, IReadOnlyDictionary<TKey, TValue>? y)
{
if (ReferenceEquals(x, y))
return true;

if (x is null || y is null)
return false;

return x.OrderBy(p => p.Key, KeySortComparer).SequenceEqual(y.OrderBy(p => p.Key, KeySortComparer), PairComparer);
}

/// <inheritdoc />
public int GetHashCode(IReadOnlyDictionary<TKey, TValue> obj)
{
if (obj == null)
return 0;

var hashCode = new HashCode();

foreach (var pair in obj.OrderBy(p => p.Key, KeySortComparer))
{
hashCode.Add(pair.Key, KeyComparer);
hashCode.Add(pair.Value, ValueComparer);
}

return hashCode.ToHashCode();
}

private KeyValuePairEqualityComparer PairComparer => new(KeyComparer, ValueComparer);

private sealed class KeyValuePairEqualityComparer(IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
: IEqualityComparer<KeyValuePair<TKey, TValue>>
{
public bool Equals(KeyValuePair<TKey, TValue> x, KeyValuePair<TKey, TValue> y) =>
keyComparer.Equals(x.Key, y.Key) && valueComparer.Equals(x.Value, y.Value);

public int GetHashCode(KeyValuePair<TKey, TValue> obj) =>
HashCode.Combine(keyComparer.GetHashCode(obj.Key!), valueComparer.GetHashCode(obj.Value!));
}

// Provides a strict total order for keys that have no natural IComparer<TKey>,
// using hash code as tiebreaker when the natural comparer returns 0 for distinct keys.
private sealed class HashTiebreakerComparer(IEqualityComparer<TKey> equalityComparer) : IComparer<TKey>
{
private static readonly IComparer<TKey> _natural = Comparer<TKey>.Default;

public int Compare(TKey? x, TKey? y)
{
int cmp = _natural.Compare(x, y);
if (cmp != 0) return cmp;
// Natural comparer considers them equal; break tie by hash code
int hx = x is null ? 0 : equalityComparer.GetHashCode(x);
int hy = y is null ? 0 : equalityComparer.GetHashCode(y);
return hx.CompareTo(hy);
}
}
}
81 changes: 81 additions & 0 deletions src/Equatable.Comparers/ReadOnlyDictionaryEqualityComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
namespace Equatable.Comparers;

/// <summary>
/// <see cref="IReadOnlyDictionary{TKey, TValue}"/> equality comparer instance
/// </summary>
/// <typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of the values in the dictionary.</typeparam>
public class ReadOnlyDictionaryEqualityComparer<TKey, TValue> : IEqualityComparer<IReadOnlyDictionary<TKey, TValue>>
{
/// <summary>
/// Gets the default equality comparer for specified generic argument.
/// </summary>
public static ReadOnlyDictionaryEqualityComparer<TKey, TValue> Default { get; } = new();

/// <summary>
/// Initializes a new instance of the <see cref="ReadOnlyDictionaryEqualityComparer{TKey, TValue}" /> class.
/// </summary>
public ReadOnlyDictionaryEqualityComparer() : this(EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ReadOnlyDictionaryEqualityComparer{TKey, TValue}" /> class.
/// </summary>
/// <param name="keyComparer">The <see cref="IEqualityComparer{TKey}"/> that is used to determine equality of keys in a dictionary</param>
/// <param name="valueComparer">The <see cref="IEqualityComparer{TValue}"/> that is used to determine equality of values in a dictionary</param>
/// <exception cref="ArgumentNullException"><paramref name="keyComparer"/> or <paramref name="valueComparer"/> is null</exception>
public ReadOnlyDictionaryEqualityComparer(IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
{
KeyComparer = keyComparer ?? throw new ArgumentNullException(nameof(keyComparer));
ValueComparer = valueComparer ?? throw new ArgumentNullException(nameof(valueComparer));
}

/// <summary>
/// Gets the <see cref="IEqualityComparer{TKey}"/> that is used to determine equality of keys in a dictionary
/// </summary>
public IEqualityComparer<TKey> KeyComparer { get; }

/// <summary>
/// Gets the <see cref="IEqualityComparer{TValue}"/> that is used to determine equality of values in a dictionary
/// </summary>
public IEqualityComparer<TValue> ValueComparer { get; }

/// <inheritdoc />
public bool Equals(IReadOnlyDictionary<TKey, TValue>? x, IReadOnlyDictionary<TKey, TValue>? y)
{
if (ReferenceEquals(x, y))
return true;

if (x is null || y is null)
return false;

if (x.Count != y.Count)
return false;

foreach (var pair in x)
{
if (!y.TryGetValue(pair.Key, out var value))
return false;

if (!ValueComparer.Equals(pair.Value, value))
return false;
}

return true;
}

/// <inheritdoc />
public int GetHashCode(IReadOnlyDictionary<TKey, TValue> obj)
{
if (obj == null)
return 0;

int hashCode = 0;

foreach (var pair in obj)
hashCode += HashCode.Combine(KeyComparer.GetHashCode(pair.Key!), ValueComparer.GetHashCode(pair.Value!));

return hashCode;
}
}
Loading