From af3deba44bbf6de9279edd933b1b27978c105365 Mon Sep 17 00:00:00 2001 From: Henrique Ruschel Date: Thu, 25 Sep 2025 23:00:55 -0300 Subject: [PATCH 1/3] PureOrdered Distinct --- .../src/System/Linq/Distinct.SpeedOpt.cs | 59 ++++++++++ .../System.Linq/src/System/Linq/Distinct.cs | 103 ++++++++++++++++++ .../System.Linq/src/System/Linq/OrderBy.cs | 61 +++++++++-- .../System/Linq/OrderedEnumerable.SpeedOpt.cs | 12 +- .../src/System/Linq/OrderedEnumerable.cs | 91 +++++++++++++++- .../System.Linq/tests/DistinctTests.cs | 52 +++++++++ 6 files changed, 365 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Distinct.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/Distinct.SpeedOpt.cs index 02706a3dcb6af2..53dd6e3b1d55db 100644 --- a/src/libraries/System.Linq/src/System/Linq/Distinct.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/Distinct.SpeedOpt.cs @@ -25,5 +25,64 @@ public override bool Contains(TSource value) => _comparer is null ? _source.Contains(value) : base.Contains(value); } + + private sealed partial class PureOrderedDistinctIterator + { + public override TSource[] ToArray() + { + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + + builder.AddNonICollectionRangeInlined(this); + + TSource[] result = builder.ToArray(); + builder.Dispose(); + + return result; + } + + public override List ToList() + { + SegmentedArrayBuilder.ScratchBuffer scratch = default; + SegmentedArrayBuilder builder = new(scratch); + + builder.AddNonICollectionRangeInlined(this); + + List result = builder.ToList(); + builder.Dispose(); + + return result; + } + + public override int GetCount(bool onlyIfCheap) + { + if (onlyIfCheap) + { + return -1; + } + + + int count = 0; + + using Iterator enumerator = this.GetEnumerator(); + + while (enumerator.MoveNext()) + { + count++; + } + + return count; + } + + public override TSource? TryGetFirst(out bool found) => _source.TryGetFirst(out found); + + public override bool Contains(TSource value) => + // If we're using the default comparer, then source.Distinct().Contains(value) is no different from + // source.Contains(value), as the Distinct() won't remove anything that could have caused + // Contains to return true. If, however, there is a custom comparer, Distinct might remove + // the elements that would have matched, and thus we can't skip it. + _comparer is null ? _source.Contains(value) : + base.Contains(value); + } } } diff --git a/src/libraries/System.Linq/src/System/Linq/Distinct.cs b/src/libraries/System.Linq/src/System/Linq/Distinct.cs index 0408f51da4ba2d..e82478507e762c 100644 --- a/src/libraries/System.Linq/src/System/Linq/Distinct.cs +++ b/src/libraries/System.Linq/src/System/Linq/Distinct.cs @@ -22,6 +22,11 @@ public static IEnumerable Distinct(this IEnumerable s return []; } + if (source is PureOrderedIterator pureOrderedIterator && (comparer is null || comparer == EqualityComparer.Default)) + { + return new PureOrderedDistinctIterator(pureOrderedIterator); + } + return new DistinctIterator(source, comparer); } @@ -158,5 +163,103 @@ public override void Dispose() base.Dispose(); } } + + /// + /// An iterator that yields the distinct values in an . + /// + /// The type of the source PureOrderedDistinctIterator. + /// + private sealed partial class PureOrderedDistinctIterator : Iterator + { + private readonly PureOrderedIterator _source; + private EqualityComparer? _comparer; + private Iterator? _enumerator; + + private const int _valueTypeState = 2; + private const int _referenceTypeState = 3; + public PureOrderedDistinctIterator(PureOrderedIterator source) + { + Debug.Assert(source is not null); + _source = source; + } + + private protected override Iterator Clone() => new PureOrderedDistinctIterator(_source); + + public override bool MoveNext() + { + switch (_state) + { + case 1: + _enumerator = _source.GetEnumerator(); + if (!_enumerator.MoveNext()) + { + Dispose(); + return false; + } + + TSource element = _enumerator.Current; + _current = element; + + if (typeof(TSource).IsValueType) + { + _state = _valueTypeState; + } + else + { + _comparer = EqualityComparer.Default; + _state = _referenceTypeState; + } + + return true; + case _valueTypeState: + // Value types + Debug.Assert(_enumerator is not null); + while (_enumerator.MoveNext()) + { + element = _enumerator.Current; + if (!EqualityComparer.Default.Equals(_current, element)) + { + _current = element; + return true; + } + } + + break; + + case _referenceTypeState: + // Reference types + Debug.Assert(_enumerator is not null); + Debug.Assert(_comparer is not null); + EqualityComparer comparer = _comparer; + while (_enumerator.MoveNext()) + { + element = _enumerator.Current; + if (!comparer.Equals(_current, element)) + { + _current = element; + return true; + } + } + + break; + } + + Dispose(); + return false; + } + + public override void Dispose() + { + if (_enumerator is not null) + { + _enumerator.Dispose(); + _enumerator = null; + } + + _comparer = null; + + base.Dispose(); + } + } } } diff --git a/src/libraries/System.Linq/src/System/Linq/OrderBy.cs b/src/libraries/System.Linq/src/System/Linq/OrderBy.cs index b276539109973d..ce798fb974021e 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderBy.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderBy.cs @@ -42,10 +42,17 @@ public static IOrderedEnumerable Order(this IEnumerable source) => /// /// If comparer is , the default comparer is used to compare elements. /// - public static IOrderedEnumerable Order(this IEnumerable source, IComparer? comparer) => - TypeIsImplicitlyStable() && (comparer is null || comparer == Comparer.Default) ? - new ImplicitlyStableOrderedIterator(source, descending: false) : - OrderBy(source, EnumerableSorter.IdentityFunc, comparer); + public static IOrderedEnumerable Order(this IEnumerable source, IComparer? comparer) + { + if (TypeCanBePureOrdered() && (comparer is null || comparer == Comparer.Default)) + { + return TypeIsImplicitlyStable() ? + new ImplicitlyStableOrderedIterator(source, descending: false) : + new PureOrderedIteratorImpl(source, descending: false); + } + + return OrderBy(source, EnumerableSorter.IdentityFunc, comparer); + } public static IOrderedEnumerable OrderBy(this IEnumerable source, Func keySelector) => new OrderedIterator(source, keySelector, null, false, null); @@ -87,10 +94,17 @@ public static IOrderedEnumerable OrderDescending(this IEnumerable sourc /// /// If comparer is , the default comparer is used to compare elements. /// - public static IOrderedEnumerable OrderDescending(this IEnumerable source, IComparer? comparer) => - TypeIsImplicitlyStable() && (comparer is null || comparer == Comparer.Default) ? - new ImplicitlyStableOrderedIterator(source, descending: true) : - OrderByDescending(source, EnumerableSorter.IdentityFunc, comparer); + public static IOrderedEnumerable OrderDescending(this IEnumerable source, IComparer? comparer) + { + if (TypeCanBePureOrdered() && (comparer is null || comparer == Comparer.Default)) + { + return TypeIsImplicitlyStable() ? + new ImplicitlyStableOrderedIterator(source, descending: true) : + new PureOrderedIteratorImpl(source, descending: true); + } + + return OrderByDescending(source, EnumerableSorter.IdentityFunc, comparer); + } public static IOrderedEnumerable OrderByDescending(this IEnumerable source, Func keySelector) => new OrderedIterator(source, keySelector, null, true, null); @@ -148,6 +162,37 @@ internal static bool TypeIsImplicitlyStable() t = typeof(T).GetEnumUnderlyingType(); } + return NonEnumTypeIsImplicitlyStable(t); + } + + /// A type can be pure ordered when every single time equal elements is side by side: [ (someVal), (someVal), (otherVal), (otherVal) ] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TypeCanBePureOrdered() + { + Type t = typeof(T); + + Type? nullableUnderlyingType = Nullable.GetUnderlyingType(t); + if (nullableUnderlyingType != null) + { + t = nullableUnderlyingType; + } + + if (typeof(T).IsEnum) + { + t = typeof(T).GetEnumUnderlyingType(); + } + + return NonEnumTypeIsImplicitlyStable(t) || + t == typeof(Half) || t == typeof(float) || + t == typeof(double) || t == typeof(decimal) || + t == typeof(string) || t == typeof(Guid) || + t == typeof(DateTime) || t == typeof(DateTimeOffset) || + t == typeof(TimeSpan); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool NonEnumTypeIsImplicitlyStable(Type t) + { // Check for integral primitive types that compare equally iff they have the same bit pattern. // bool is included because, even though technically it can have 256 different values, anything // other than 0/1 is only producible using unsafe code. It's tempting to include a type like string diff --git a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs index 1a3d09ded319a5..8431df244ba302 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.SpeedOpt.cs @@ -250,6 +250,10 @@ private TElement Last(TElement[] items) } } + private abstract partial class PureOrderedIterator : OrderedIterator + { + } + private sealed partial class OrderedIterator : OrderedIterator { // For complicated cases, rely on the base implementation that's more comprehensive. @@ -361,7 +365,11 @@ private sealed partial class OrderedIterator : OrderedIterator : OrderedIterator + private sealed partial class PureOrderedIteratorImpl : PureOrderedIterator + { + } + + private sealed partial class ImplicitlyStableOrderedIterator : PureOrderedIterator { public override TElement[] ToArray() { @@ -461,7 +469,7 @@ public override bool MoveNext() { int state = _state; - Initialized: + Initialized: if (state > 1) { Debug.Assert(_buffer is not null); diff --git a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs index 447094cc867f9e..f7cf65e12dd15c 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs @@ -61,6 +61,20 @@ IOrderedEnumerable IOrderedEnumerable.CreateOrderedEnumerabl } } + /// + /// An ordered enumerable that for the same input always produces the same output + /// + /// + /// A pure ordered enumerable means that always equal elements will be side by side: [ (someVal), (someVal), (otherVal), (otherVal) ], see github.com/dotnet/runtime/issues/120125 + /// + private abstract partial class PureOrderedIterator : OrderedIterator + { + protected PureOrderedIterator(IEnumerable source) : base(source) + { + Debug.Assert(TypeCanBePureOrdered()); + } + } + private sealed partial class OrderedIterator : OrderedIterator { private readonly OrderedIterator? _parent; @@ -122,7 +136,78 @@ public override bool MoveNext() { int state = _state; - Initialized: + Initialized: + if (state > 1) + { + Debug.Assert(_buffer is not null); + Debug.Assert(_map is not null); + Debug.Assert(_map.Length == _buffer.Length); + + int[] map = _map; + int i = state - 2; + if ((uint)i < (uint)map.Length) + { + _current = _buffer[map[i]]; + _state++; + return true; + } + } + else if (state == 1) + { + TElement[] buffer = _source.ToArray(); + if (buffer.Length != 0) + { + _map = SortedMap(buffer); + _buffer = buffer; + _state = state = 2; + goto Initialized; + } + } + + Dispose(); + return false; + } + + public override void Dispose() + { + _buffer = null; + _map = null; + base.Dispose(); + } + } + + private sealed partial class PureOrderedIteratorImpl : PureOrderedIterator + { + private readonly bool _descending; + private TElement[]? _buffer; + private int[]? _map; + + internal PureOrderedIteratorImpl(IEnumerable source, bool descending) : + base(source) + { + if (source is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + } + + _descending = descending; + } + + private protected override Iterator Clone() => new PureOrderedIteratorImpl(_source, _descending); + + internal override EnumerableSorter GetEnumerableSorter(EnumerableSorter? next) => + new EnumerableSorter(EnumerableSorter.IdentityFunc, Comparer.Default, _descending, next); + + internal override CachingComparer GetComparer(CachingComparer? childComparer) => + childComparer is null ? + new CachingComparer(EnumerableSorter.IdentityFunc, Comparer.Default, _descending) : + new CachingComparerWithChild(EnumerableSorter.IdentityFunc, Comparer.Default, _descending, childComparer); + + public override bool MoveNext() + { + int state = _state; + + Initialized: if (state > 1) { Debug.Assert(_buffer is not null); @@ -163,7 +248,7 @@ public override void Dispose() } /// An ordered enumerable used by Order/OrderDescending for Ts that are bitwise indistinguishable for any considered equal. - private sealed partial class ImplicitlyStableOrderedIterator : OrderedIterator + private sealed partial class ImplicitlyStableOrderedIterator : PureOrderedIterator { private readonly bool _descending; private TElement[]? _buffer; @@ -195,7 +280,7 @@ public override bool MoveNext() int state = _state; TElement[]? buffer; - Initialized: + Initialized: if (state > 1) { buffer = _buffer; diff --git a/src/libraries/System.Linq/tests/DistinctTests.cs b/src/libraries/System.Linq/tests/DistinctTests.cs index f0e29e65f148fd..2233ea720ae09d 100644 --- a/src/libraries/System.Linq/tests/DistinctTests.cs +++ b/src/libraries/System.Linq/tests/DistinctTests.cs @@ -270,6 +270,58 @@ public void RepeatEnumerating() Assert.Equal(result, result); } + [Fact] + public void PureOrderedDistinct() + { + int[] source = [1, 1, 1, 2, 2, 2, 3, 3, 3]; + int[] expected = [1, 2, 3]; + + Assert.Equal(expected, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinctToArray() + { + int[] source = [1, 1, 1, 2, 2, 2, 3, 3, 3]; + int[] expected = [1, 2, 3]; + + Assert.Equal(expected, source.Order().Distinct().ToArray()); + } + + [Fact] + public void PureOrderedDistinctToList() + { + int[] source = [1, 1, 1, 2, 2, 2, 3, 3, 3]; + int[] expected = [1, 2, 3]; + + Assert.Equal(expected, source.Order().Distinct().ToList()); + } + + [Fact] + public void PureOrderedDistinctCount() + { + int[] source = [1, 1, 1, 2, 2, 2, 3, 3, 3]; + + Assert.Equal(3, source.Order().Distinct().Count()); + } + + [Fact] + public void PureOrderedDistinctAllUnique() + { + int[] source = [-5, 0, 2, 6, 9, 10]; + + Assert.Equal(source, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinctAllDuplicates() + { + int[] source = [5, 5, 5, 5, 5, 5]; + int[] expected = [5]; + + Assert.Equal(expected, source.Order().Distinct()); + } + [Fact] public void DistinctBy_SourceNull_ThrowsArgumentNullException() { From e809cc063cf01d52d8b8f2d05582d821e5142728 Mon Sep 17 00:00:00 2001 From: Henrique Ruschel Date: Thu, 25 Sep 2025 23:40:16 -0300 Subject: [PATCH 2/3] Removed trailing whitespace --- src/libraries/System.Linq/src/System/Linq/Distinct.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Linq/src/System/Linq/Distinct.cs b/src/libraries/System.Linq/src/System/Linq/Distinct.cs index e82478507e762c..77e1fccdc31893 100644 --- a/src/libraries/System.Linq/src/System/Linq/Distinct.cs +++ b/src/libraries/System.Linq/src/System/Linq/Distinct.cs @@ -168,7 +168,6 @@ public override void Dispose() /// An iterator that yields the distinct values in an . /// /// The type of the source PureOrderedDistinctIterator. - /// private sealed partial class PureOrderedDistinctIterator : Iterator { private readonly PureOrderedIterator _source; From bdab4720c9f397a2922fb28210e1e5cba0b66b52 Mon Sep 17 00:00:00 2001 From: Henrique Ruschel Date: Tue, 30 Sep 2025 16:50:41 -0300 Subject: [PATCH 3/3] Adjusted names in tests, tests for more types, and included DateOnly, TimeOnly --- .../System.Linq/src/System/Linq/OrderBy.cs | 5 +- .../System.Linq/tests/DistinctTests.cs | 176 +++++++++++++++++- 2 files changed, 173 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/OrderBy.cs b/src/libraries/System.Linq/src/System/Linq/OrderBy.cs index ce798fb974021e..d91463212034a1 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderBy.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderBy.cs @@ -165,7 +165,7 @@ internal static bool TypeIsImplicitlyStable() return NonEnumTypeIsImplicitlyStable(t); } - /// A type can be pure ordered when every single time equal elements is side by side: [ (someVal), (someVal), (otherVal), (otherVal) ] + /// A type can be pure ordered when every single time equal elements is side by side: [ (someVal), (someVal), (otherVal), (otherVal) ] see: #120125 [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool TypeCanBePureOrdered() { @@ -187,7 +187,8 @@ internal static bool TypeCanBePureOrdered() t == typeof(double) || t == typeof(decimal) || t == typeof(string) || t == typeof(Guid) || t == typeof(DateTime) || t == typeof(DateTimeOffset) || - t == typeof(TimeSpan); + t == typeof(TimeSpan) || t == typeof(TimeOnly) || + t == typeof(DateOnly); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Linq/tests/DistinctTests.cs b/src/libraries/System.Linq/tests/DistinctTests.cs index 2233ea720ae09d..bd405af4edcb7e 100644 --- a/src/libraries/System.Linq/tests/DistinctTests.cs +++ b/src/libraries/System.Linq/tests/DistinctTests.cs @@ -271,7 +271,7 @@ public void RepeatEnumerating() } [Fact] - public void PureOrderedDistinct() + public void PureOrderedDistinct_Iterator() { int[] source = [1, 1, 1, 2, 2, 2, 3, 3, 3]; int[] expected = [1, 2, 3]; @@ -280,7 +280,171 @@ public void PureOrderedDistinct() } [Fact] - public void PureOrderedDistinctToArray() + public void PureOrderedDistinct_NullableType_Iterator() + { + int?[] source = [null, null, 1, 1, 1, 2, 2, 2, 3, 3, 3]; + int?[] expected = [null, 1, 2, 3]; + + Assert.Equal(expected, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinct_DateTime_Iterator() + { + DateTime[] source = + [ + DateTime.Parse("2025-09-30 15:45:00"), DateTime.Parse("2025-09-30 15:45:00"), + DateTime.Parse("2025-09-30 15:50:00"), DateTime.Parse("2025-09-30 15:50:00"), DateTime.Parse("2025-09-30 15:50:00"), + DateTime.Parse("2025-09-30 15:51:00"), DateTime.Parse("2025-09-30 15:51:00"), + DateTime.Parse("2025-09-30 15:59:00"), + ]; + DateTime[] expected = [DateTime.Parse("2025-09-30 15:45:00"), DateTime.Parse("2025-09-30 15:50:00"), DateTime.Parse("2025-09-30 15:51:00"), DateTime.Parse("2025-09-30 15:59:00")]; + + Assert.Equal(expected, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinct_Float_Iterator() + { + float[] source = + [ + 1.1F, 1.1F, + 2.1F, 2.1F, 2.1F, + 2.7F, 2.7F, + 4.4F, + ]; + float[] expected = [1.1F, 2.1F, 2.7F, 4.4F]; + + Assert.Equal(expected, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinct_Double_Iterator() + { + double[] source = + [ + 1.1D, 1.1D, + 2.1D, 2.1D, 2.1D, + 2.7D, 2.7D, + 4.4D, + ]; + double[] expected = [1.1D, 2.1D, 2.7D, 4.4D]; + + Assert.Equal(expected, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinct_Decimal_Iterator() + { + decimal[] source = + [ + 1.1M, 1.1M, + 2.1M, 2.1M, 2.1M, + 2.7M, 2.7M, + 4.4M, + ]; + decimal[] expected = [1.1M, 2.1M, 2.7M, 4.4M]; + + Assert.Equal(expected, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinct_Guid_Iterator() + { + Guid[] source = + [ + Guid.Parse("10000000-0000-0000-0000-000000000000"), Guid.Parse("10000000-0000-0000-0000-000000000000"), + Guid.Parse("20000000-0000-0000-0000-000000000000"), Guid.Parse("20000000-0000-0000-0000-000000000000"), Guid.Parse("20000000-0000-0000-0000-000000000000"), + Guid.Parse("30000000-0000-0000-0000-000000000000"), Guid.Parse("30000000-0000-0000-0000-000000000000"), + Guid.Parse("40000000-0000-0000-0000-000000000000"), + ]; + Guid[] expected = [Guid.Parse("10000000-0000-0000-0000-000000000000"), Guid.Parse("20000000-0000-0000-0000-000000000000"), Guid.Parse("30000000-0000-0000-0000-000000000000"), Guid.Parse("40000000-0000-0000-0000-000000000000")]; + + Assert.Equal(expected, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinct_String_Iterator() + { + string[] source = + [ + "alpha", + "beta", "beta", + "delta", "delta", + "gamma", "gamma", + ]; + + string[] expected = ["alpha", "beta", "delta", "gamma"]; + + Assert.Equal(expected, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinct_DateTimeOffset_Iterator() + { + DateTimeOffset[] source = + [ + DateTimeOffset.Parse("2025-09-29T09:00:00+00:00"), DateTimeOffset.Parse("2025-09-29T09:00:00+00:00"), DateTimeOffset.Parse("2025-09-29T08:00:00-01:00"), // Equals because of the timezone + DateTimeOffset.Parse("2025-09-29T10:00:00+00:00"), + DateTimeOffset.Parse("2025-09-29T11:00:00+00:00"), DateTimeOffset.Parse("2025-09-29T11:00:00+00:00"), + DateTimeOffset.Parse("2025-09-29T12:00:00+00:00"), DateTimeOffset.Parse("2025-09-29T12:00:00+00:00"), + ]; + + DateTimeOffset[] expected = [DateTimeOffset.Parse("2025-09-29T09:00:00+00:00"), DateTimeOffset.Parse("2025-09-29T10:00:00+00:00"), DateTimeOffset.Parse("2025-09-29T11:00:00+00:00"), DateTimeOffset.Parse("2025-09-29T12:00:00+00:00")]; + + Assert.Equal(expected, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinct_TimeSpan_Iterator() + { + TimeSpan[] source = + [ + TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3), + TimeSpan.FromSeconds(4), TimeSpan.FromSeconds(4), TimeSpan.FromSeconds(4), + ]; + + TimeSpan[] expected = [TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(4)]; + + Assert.Equal(expected, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinct_TimeOnly_Iterator() + { + TimeOnly[] source = + [ + new TimeOnly(4, 00), new TimeOnly(4, 00), + new TimeOnly(4, 30), + new TimeOnly(4, 44), new TimeOnly(4, 44), + new TimeOnly(5, 00), new TimeOnly(5, 00), new TimeOnly(5, 00), + ]; + + TimeOnly[] expected = [new TimeOnly(4, 00), new TimeOnly(4, 30), new TimeOnly(4, 44), new TimeOnly(5, 00)]; + + Assert.Equal(expected, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinct_DateOnly_Iterator() + { + DateOnly[] source = + [ + new DateOnly(2025, 09, 1), new DateOnly(2025, 09, 1), + new DateOnly(2025, 09, 2), + new DateOnly(2025, 09, 3), new DateOnly(2025, 09, 3), + new DateOnly(2025, 09, 4), new DateOnly(2025, 09, 4), new DateOnly(2025, 09, 4), + ]; + + DateOnly[] expected = [new DateOnly(2025, 09, 1), new DateOnly(2025, 09, 2), new DateOnly(2025, 09, 3), new DateOnly(2025, 09, 4)]; + + Assert.Equal(expected, source.Order().Distinct()); + } + + [Fact] + public void PureOrderedDistinct_ToArray() { int[] source = [1, 1, 1, 2, 2, 2, 3, 3, 3]; int[] expected = [1, 2, 3]; @@ -289,7 +453,7 @@ public void PureOrderedDistinctToArray() } [Fact] - public void PureOrderedDistinctToList() + public void PureOrderedDistinct_ToList() { int[] source = [1, 1, 1, 2, 2, 2, 3, 3, 3]; int[] expected = [1, 2, 3]; @@ -298,7 +462,7 @@ public void PureOrderedDistinctToList() } [Fact] - public void PureOrderedDistinctCount() + public void PureOrderedDistinct_Count() { int[] source = [1, 1, 1, 2, 2, 2, 3, 3, 3]; @@ -306,7 +470,7 @@ public void PureOrderedDistinctCount() } [Fact] - public void PureOrderedDistinctAllUnique() + public void PureOrderedDistinct_AllUnique_Should_Not_Change() { int[] source = [-5, 0, 2, 6, 9, 10]; @@ -314,7 +478,7 @@ public void PureOrderedDistinctAllUnique() } [Fact] - public void PureOrderedDistinctAllDuplicates() + public void PureOrderedDistinct_Should_Remove_All_Duplicates() { int[] source = [5, 5, 5, 5, 5, 5]; int[] expected = [5];