Skip to content

Commit a59c9eb

Browse files
authored
Implement ComparerEquatableArray<T> (#48)
1 parent b342c34 commit a59c9eb

4 files changed

Lines changed: 1128 additions & 230 deletions

File tree

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
using System.Collections;
2+
using System.Collections.Immutable;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Runtime.CompilerServices;
5+
using System.Runtime.InteropServices;
6+
7+
#pragma warning disable IDE0303 // Simplify collection initialization
8+
#pragma warning disable CA1305 // Specify IFormatProvider
9+
10+
namespace Singulink.Collections;
11+
12+
/// <summary>
13+
/// Provides methods for creating an <see cref="ComparerEquatableArray{T}"/>.
14+
/// </summary>
15+
public static class ComparerEquatableArray
16+
{
17+
/// <summary>
18+
/// Returns an instance of the <see cref="ComparerEquatableArray{T}"/> class from the given collection with the default comparer.
19+
/// </summary>
20+
/// <remarks>
21+
/// Note: values of the collection are assumed to be immutable and interchangeable (that is, the standard equality and hash code contracts must be held, but
22+
/// also must be consistent indefinitely, and the value you read out of the array is only guaranteed to be equal to the one passed in, not necessarily the
23+
/// same instance or bits).
24+
/// </remarks>
25+
public static ComparerEquatableArray<T> Create<T>(ImmutableArray<T> items) => Create((IEqualityComparer<T>)null, items);
26+
27+
/// <inheritdoc cref="Create{T}(ImmutableArray{T})" />
28+
public static ComparerEquatableArray<T> Create<T>(IEnumerable<T> items) => Create((IEqualityComparer<T>)null, items);
29+
30+
/// <inheritdoc cref="Create{T}(ImmutableArray{T})" />
31+
public static ComparerEquatableArray<T> Create<T>(params ReadOnlySpan<T> items) => Create(null, items);
32+
33+
/// <inheritdoc cref="Create{T}(ImmutableArray{T})" />
34+
public static ComparerEquatableArray<T> Create<T>(params T[] items) => Create((IEqualityComparer<T>)null, items);
35+
36+
/// <summary>
37+
/// Returns an instance of the <see cref="ComparerEquatableArray{T}"/> class from the given collection with the specified comparer.
38+
/// </summary>
39+
/// <remarks>
40+
/// <para>Note: values of the collection are assumed to be immutable and interchangeable (that is, the standard equality and hash code contracts must be held, but
41+
/// also must be consistent indefinitely, and the value you read out of the array is only guaranteed to be equal to the one passed in, not necessarily the
42+
/// same instance or bits).</para>
43+
/// <para>
44+
/// Note: the values for the comparer are compared by reference equality, but with <see langword="null" /> considered equivalent to
45+
/// <see cref="EqualityComparer{T}.Default" />.</para>
46+
/// </remarks>
47+
public static ComparerEquatableArray<T> Create<T>(IEqualityComparer<T>? comparer, ImmutableArray<T> items)
48+
{
49+
if ((comparer == null || comparer == EqualityComparer<T>.Default) && (items == default || items.Length == 0))
50+
return ComparerEquatableArray<T>.Empty;
51+
52+
return new(items, comparer);
53+
}
54+
55+
/// <inheritdoc cref="Create{T}(IEqualityComparer{T}?, ImmutableArray{T})" />
56+
public static ComparerEquatableArray<T> Create<T>(IEqualityComparer<T>? comparer, IEnumerable<T> items)
57+
{
58+
// Optimize for common simple cases:
59+
60+
if (items is ImmutableArray<T> immutableArray)
61+
return Create(comparer, immutableArray);
62+
63+
if (items is ComparerEquatableArray<T> comparerEquatableArray)
64+
{
65+
if (comparerEquatableArray.Comparer == (comparer ?? EqualityComparer<T>.Default))
66+
return comparerEquatableArray;
67+
else
68+
return Create(comparer, comparerEquatableArray.UnderlyingArray);
69+
}
70+
71+
if (items is EquatableArray<T> equatableArray)
72+
{
73+
return Create(comparer, equatableArray.UnderlyingArray);
74+
}
75+
76+
if (
77+
(comparer == null || comparer == EqualityComparer<T>.Default) &&
78+
#if NET
79+
items.TryGetNonEnumeratedCount(out int count) && count == 0)
80+
#else
81+
items is ICollection<T> { Count: 0 } or ICollection { Count: 0 })
82+
#endif
83+
{
84+
return ComparerEquatableArray<T>.Empty;
85+
}
86+
87+
// Otherwise, just create from the full collection:
88+
return new ComparerEquatableArray<T>(ImmutableArray.CreateRange(items), comparer);
89+
}
90+
91+
/// <inheritdoc cref="Create{T}(IEqualityComparer{T}?, ImmutableArray{T})" />
92+
public static ComparerEquatableArray<T> Create<T>(IEqualityComparer<T>? comparer, params ReadOnlySpan<T> items)
93+
{
94+
if (items.Length == 0 && (comparer == null || comparer == EqualityComparer<T>.Default))
95+
return ComparerEquatableArray<T>.Empty;
96+
97+
#if NET
98+
var array = GC.AllocateUninitializedArray<T>(items.Length);
99+
#else
100+
var array = new T[items.Length];
101+
#endif
102+
103+
items.CopyTo(array);
104+
105+
#if NET8_0_OR_GREATER
106+
var immutableArray = ImmutableCollectionsMarshal.AsImmutableArray(array);
107+
#else
108+
var immutableArray = Unsafe.As<T[], ImmutableArray<T>>(ref array);
109+
#endif
110+
111+
return new ComparerEquatableArray<T>(immutableArray, comparer);
112+
}
113+
114+
/// <inheritdoc cref="Create{T}(IEqualityComparer{T}?, ImmutableArray{T})" />
115+
public static ComparerEquatableArray<T> Create<T>(IEqualityComparer<T>? comparer, params T[] items) => Create(comparer, (ReadOnlySpan<T>)items);
116+
117+
/// <inheritdoc cref="Create{T}(IEqualityComparer{T}?, ImmutableArray{T})" />
118+
public static ComparerEquatableArray<T> ToComparerEquatableArray<T>(this ImmutableArray<T> value, IEqualityComparer<T>? comparer = null) => Create(comparer, value);
119+
120+
/// <inheritdoc cref="Create{T}(IEqualityComparer{T}?, ImmutableArray{T})" />
121+
public static ComparerEquatableArray<T> ToEquatableArray<T>(this IEnumerable<T> value, IEqualityComparer<T>? comparer = null) => Create(comparer, value);
122+
123+
/// <inheritdoc cref="Create{T}(IEqualityComparer{T}?, ImmutableArray{T})" />
124+
public static ComparerEquatableArray<T> ToEquatableArray<T>(this ReadOnlySpan<T> value, IEqualityComparer<T>? comparer = null) => Create(comparer, value);
125+
}
126+
127+
/// <summary>
128+
/// An immutable array wrapper that provides value-based equality semantics with a custom comparer.
129+
/// </summary>
130+
/// <remarks>
131+
/// Note: values of the array are assumed to be immutable and interchangeable (that is, the standard equality and hash code contracts must be held, but also
132+
/// must be consistent indefinitely, and the value you read out of the array is only guaranteed to be equal to the one passed in, not necessarily the same
133+
/// instance or bits).
134+
/// </remarks>
135+
[CollectionBuilder(typeof(ComparerEquatableArray), "Create")]
136+
public sealed class ComparerEquatableArray<T> : IReadOnlyList<T>, IList<T>, IEquatable<ComparerEquatableArray<T>>, IFormattable
137+
{
138+
// See EquatableArrayImpl for explanation.
139+
private static nint _staticCreateIndex;
140+
141+
// Comparer field:
142+
private readonly IEqualityComparer<T>? _comparer;
143+
144+
// Impl field:
145+
private EquatableArrayImpl<T> _impl;
146+
147+
/// <summary>
148+
/// Initializes a new instance of the <see cref="ComparerEquatableArray{T}"/> class from the given immutable array and comparer.
149+
/// </summary>
150+
/// <remarks>
151+
/// <para>
152+
/// Note: values of the array are assumed to be immutable and interchangeable (that is, the standard equality and hash code contracts must be held, but also
153+
/// must be consistent indefinitely, and the value you read out of the array is only guaranteed to be equal to the one passed in, not necessarily the same
154+
/// instance or bits). Note: this constructor does not de-duplicate against existing instances when created (however, it can still later) - it is a quick
155+
/// constructor that just copies the provided value directly, other than ensuring non-<see langword="null" /> - use methods on
156+
/// <see cref="ComparerEquatableArray" /> to get de-duplication.</para>
157+
/// <para>
158+
/// Note: the values for the comparer are compared by reference equality, but with <see langword="null" /> considered equivalent to
159+
/// <see cref="EqualityComparer{T}.Default" />.</para>
160+
/// </remarks>
161+
public ComparerEquatableArray(ImmutableArray<T> array, IEqualityComparer<T>? comparer = null)
162+
{
163+
_impl = new(array, ref _staticCreateIndex);
164+
_comparer = comparer == EqualityComparer<T>.Default ? null : comparer;
165+
}
166+
167+
/// <summary>
168+
/// Gets the shared empty instance of the <see cref="ComparerEquatableArray{T}"/> class with the default comparer.
169+
/// </summary>
170+
public static ComparerEquatableArray<T> Empty { get; } = new(ImmutableArray<T>.Empty);
171+
172+
/// <inheritdoc cref="EquatableArray{T}.UnderlyingArray" />
173+
public ImmutableArray<T> UnderlyingArray => _impl.UnderlyingArray;
174+
175+
/// <summary>
176+
/// Gets the comparer used for value-based equality.
177+
/// </summary>
178+
public IEqualityComparer<T> Comparer => _comparer ?? EqualityComparer<T>.Default;
179+
180+
/// <inheritdoc cref="EquatableArray{T}.UnderlyingArray" />
181+
public override int GetHashCode() => _impl.GetHashCode(_comparer);
182+
183+
/// <inheritdoc cref="Equals(object?)" />
184+
public bool Equals([NotNullWhen(true)] ComparerEquatableArray<T>? other)
185+
{
186+
// Check simple cases:
187+
if (other is null)
188+
return false;
189+
if (ReferenceEquals(this, other))
190+
return true;
191+
if (_comparer != other._comparer)
192+
return false;
193+
194+
// Call into impl:
195+
return _impl.Equals(ref other._impl, _comparer);
196+
}
197+
198+
/// <summary>Determines whether the specified object is equal to the current object.</summary>
199+
/// <param name="obj">The object to compare with the current object.</param>
200+
/// <returns><see langword="true" /> if the specified object is equal to the current object; otherwise, <see langword="false" />.</returns>
201+
/// <remarks>Returns <see langword="false" /> if the two instances of <see cref="ComparerEquatableArray{T}"/> have different comparers.</remarks>
202+
public override bool Equals(object? obj) => Equals(obj as ComparerEquatableArray<T>);
203+
204+
/// <summary>
205+
/// Value-based equality operator.
206+
/// </summary>
207+
/// <remarks>Returns <see langword="false" /> if the two instances of <see cref="ComparerEquatableArray{T}"/> have different comparers.</remarks>
208+
public static bool operator ==(ComparerEquatableArray<T>? left, ComparerEquatableArray<T>? right) => Equals(left, right);
209+
210+
/// <summary>
211+
/// Value-based inequality operator.
212+
/// </summary>
213+
/// <remarks>Returns <see langword="false" /> if the two instances of <see cref="ComparerEquatableArray{T}"/> have different comparers.</remarks>
214+
public static bool operator !=(ComparerEquatableArray<T>? left, ComparerEquatableArray<T>? right) => !(left == right);
215+
216+
/// <inheritdoc cref="EquatableArray{T}.Length" />
217+
public int Length => _impl.Length;
218+
219+
/// <inheritdoc cref="EquatableArray{T}.IsEmpty" />
220+
public bool IsEmpty => _impl.IsEmpty;
221+
222+
/// <inheritdoc cref="EquatableArray{T}.this[int]" />
223+
public ref readonly T this[int index] => ref _impl[index];
224+
225+
/// <inheritdoc />
226+
int ICollection<T>.Count => Length;
227+
228+
/// <inheritdoc />
229+
int IReadOnlyCollection<T>.Count => Length;
230+
231+
/// <inheritdoc />
232+
bool ICollection<T>.IsReadOnly => true;
233+
234+
/// <inheritdoc />
235+
T IList<T>.this[int index]
236+
{
237+
get => this[index];
238+
set => EquatableArrayImpl.ThrowReadOnlyException();
239+
}
240+
241+
/// <inheritdoc />
242+
T IReadOnlyList<T>.this[int index] => this[index];
243+
244+
/// <inheritdoc />
245+
IEnumerator<T> IEnumerable<T>.GetEnumerator() => _impl.IEnumerableTGetEnumerator();
246+
247+
/// <inheritdoc />
248+
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<T>)this).GetEnumerator();
249+
250+
/// <inheritdoc />
251+
void IList<T>.Insert(int index, T item) => EquatableArrayImpl.ThrowReadOnlyException();
252+
253+
/// <inheritdoc />
254+
void IList<T>.RemoveAt(int index) => EquatableArrayImpl.ThrowReadOnlyException();
255+
256+
/// <inheritdoc />
257+
void ICollection<T>.Add(T item) => EquatableArrayImpl.ThrowReadOnlyException();
258+
259+
/// <inheritdoc />
260+
void ICollection<T>.Clear() => EquatableArrayImpl.ThrowReadOnlyException();
261+
262+
/// <inheritdoc />
263+
void ICollection<T>.CopyTo(T[] array, int arrayIndex) => CopyTo(array, arrayIndex);
264+
265+
/// <inheritdoc />
266+
bool ICollection<T>.Remove(T item) => EquatableArrayImpl.ThrowReadOnlyException<bool>();
267+
268+
/// <inheritdoc />
269+
public override string ToString() => _impl.ToString();
270+
271+
/// <inheritdoc cref="EquatableArray{T}.ToString(IFormatProvider?)" />
272+
public string ToString(IFormatProvider? formatProvider) => _impl.ToString(formatProvider);
273+
274+
/// <inheritdoc cref="EquatableArray{T}.ToString(string?)" />
275+
public string ToString(string? elementFormat) => _impl.ToString(elementFormat);
276+
277+
/// <inheritdoc cref="EquatableArray{T}.ToString(string?, IFormatProvider?)" />
278+
public string ToString(string? elementFormat, IFormatProvider? formatProvider) => _impl.ToString(elementFormat, formatProvider);
279+
280+
/// <inheritdoc cref="IFormattable.ToString(string?, IFormatProvider?)" />
281+
string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(format, formatProvider);
282+
283+
/// <inheritdoc cref="EquatableArray{T}.Enumerator" />
284+
public struct Enumerator
285+
{
286+
private EquatableArrayImpl<T>.Enumerator _impl;
287+
288+
internal Enumerator(ComparerEquatableArray<T> array)
289+
{
290+
_impl = array._impl.GetEnumerator();
291+
}
292+
293+
/// <inheritdoc cref="EquatableArray{T}.Enumerator.Current" />
294+
public readonly T Current => _impl.Current;
295+
296+
/// <inheritdoc cref="EquatableArray{T}.Enumerator.MoveNext()" />
297+
public bool MoveNext() => _impl.MoveNext();
298+
}
299+
300+
/// <inheritdoc cref="EquatableArray{T}.AsMemory()" />
301+
public ReadOnlyMemory<T> AsMemory() => _impl.AsMemory();
302+
303+
/// <inheritdoc cref="EquatableArray{T}.AsSpan()" />
304+
public ReadOnlySpan<T> AsSpan() => _impl.AsSpan();
305+
306+
/// <inheritdoc cref="EquatableArray{T}.AsSpan(int, int)" />
307+
public ReadOnlySpan<T> AsSpan(int start, int length) => _impl.AsSpan(start, length);
308+
309+
/// <inheritdoc cref="EquatableArray{T}.AsSpan(Range)" />
310+
public ReadOnlySpan<T> AsSpan(Range range) => _impl.AsSpan(range);
311+
312+
/// <inheritdoc cref="EquatableArray{T}.Contains(T)" />
313+
public bool Contains(T item) => _impl.Contains(item, _comparer);
314+
315+
/// <inheritdoc cref="EquatableArray{T}.Contains(T, IEqualityComparer{T}?)" />
316+
public bool Contains(T item, IEqualityComparer<T>? equalityComparer) => _impl.Contains(item, equalityComparer ?? _comparer);
317+
318+
/// <inheritdoc cref="EquatableArray{T}.CopyTo(Span{T})" />
319+
public void CopyTo(Span<T> destination) => _impl.CopyTo(destination);
320+
321+
/// <inheritdoc cref="EquatableArray{T}.CopyTo(T[])" />
322+
public void CopyTo(T[] destination) => _impl.CopyTo(destination);
323+
324+
/// <inheritdoc cref="EquatableArray{T}.CopyTo(T[], int)" />
325+
public void CopyTo(T[] destination, int destinationIndex) => _impl.CopyTo(destination, destinationIndex);
326+
327+
/// <inheritdoc cref="EquatableArray{T}.CopyTo(int, T[], int, int)" />
328+
public void CopyTo(int sourceIndex, T[] destination, int destinationIndex, int length) => _impl.CopyTo(sourceIndex, destination, destinationIndex, length);
329+
330+
/// <inheritdoc cref="EquatableArray{T}.GetEnumerator()" />
331+
public Enumerator GetEnumerator() => new(this);
332+
333+
/// <inheritdoc cref="EquatableArray{T}.IndexOf(T)" />
334+
public int IndexOf(T item) => _impl.IndexOf(item, _comparer);
335+
336+
/// <inheritdoc cref="EquatableArray{T}.IndexOf(T, IEqualityComparer{T}?)" />
337+
public int IndexOf(T item, IEqualityComparer<T>? equalityComparer) => _impl.IndexOf(item, equalityComparer ?? _comparer);
338+
339+
/// <inheritdoc cref="EquatableArray{T}.IndexOf(T, int)" />
340+
public int IndexOf(T item, int startIndex) => _impl.IndexOf(item, startIndex, _comparer);
341+
342+
/// <inheritdoc cref="EquatableArray{T}.IndexOf(T, int, IEqualityComparer{T}?)" />
343+
public int IndexOf(T item, int startIndex, IEqualityComparer<T>? equalityComparer) => _impl.IndexOf(item, startIndex, equalityComparer ?? _comparer);
344+
345+
/// <inheritdoc cref="EquatableArray{T}.IndexOf(T, int, int)" />
346+
public int IndexOf(T item, int startIndex, int count) => _impl.IndexOf(item, startIndex, count, _comparer);
347+
348+
/// <inheritdoc cref="EquatableArray{T}.IndexOf(T, int, int, IEqualityComparer{T}?)" />
349+
public int IndexOf(T item, int startIndex, int count, IEqualityComparer<T>? equalityComparer) => _impl.IndexOf(item, startIndex, count, equalityComparer ?? _comparer);
350+
351+
/// <inheritdoc cref="EquatableArray{T}.LastIndexOf(T)" />
352+
public int LastIndexOf(T item) => _impl.LastIndexOf(item, _comparer);
353+
354+
/// <inheritdoc cref="EquatableArray{T}.LastIndexOf(T, IEqualityComparer{T}?)" />
355+
public int LastIndexOf(T item, IEqualityComparer<T>? equalityComparer) => _impl.LastIndexOf(item, equalityComparer ?? _comparer);
356+
357+
/// <inheritdoc cref="EquatableArray{T}.LastIndexOf(T, int)" />
358+
public int LastIndexOf(T item, int startIndex) => _impl.LastIndexOf(item, startIndex, _comparer);
359+
360+
/// <inheritdoc cref="EquatableArray{T}.LastIndexOf(T, int, IEqualityComparer{T}?)" />
361+
public int LastIndexOf(T item, int startIndex, IEqualityComparer<T>? equalityComparer) => _impl.LastIndexOf(item, startIndex, equalityComparer ?? _comparer);
362+
363+
/// <inheritdoc cref="EquatableArray{T}.LastIndexOf(T, int, int)" />
364+
public int LastIndexOf(T item, int startIndex, int count) => _impl.LastIndexOf(item, startIndex, count, _comparer);
365+
366+
/// <inheritdoc cref="EquatableArray{T}.LastIndexOf(T, int, int, IEqualityComparer{T}?)" />
367+
public int LastIndexOf(T item, int startIndex, int count, IEqualityComparer<T>? equalityComparer) => _impl.LastIndexOf(item, startIndex, count, equalityComparer ?? _comparer);
368+
369+
/// <inheritdoc cref="EquatableArray{T}.Slice(int, int)" />
370+
public ComparerEquatableArray<T> Slice(int start, int length) => _impl.Slice(start, length, out bool identical) switch
371+
{
372+
_ when identical => this,
373+
var x => ComparerEquatableArray.Create(_comparer, x),
374+
};
375+
376+
/// <summary>
377+
/// Returns an <see cref="ComparerEquatableArray{T}"/> instance with the specified comparer over the current values.
378+
/// </summary>
379+
public ComparerEquatableArray<T> WithComparer(IEqualityComparer<T>? comparer)
380+
{
381+
if (comparer == EqualityComparer<T>.Default)
382+
comparer = null;
383+
384+
if (comparer == _comparer)
385+
return this;
386+
387+
return ComparerEquatableArray.Create(comparer, UnderlyingArray);
388+
}
389+
}

0 commit comments

Comments
 (0)