diff --git a/MoreLinq.Test/DuplicatesTest.cs b/MoreLinq.Test/DuplicatesTest.cs new file mode 100644 index 000000000..2b97f471e --- /dev/null +++ b/MoreLinq.Test/DuplicatesTest.cs @@ -0,0 +1,105 @@ +#region License and Terms +// MoreLINQ - Extensions to LINQ to Objects +// Copyright (c) 2023 Julien Aspirot. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +namespace MoreLinq.Test +{ + using System.Collections.Generic; + using NUnit.Framework; + using Delegate = Delegating.Delegate; + + [TestFixture] + public class DuplicatesTest + { + [Test] + public void Duplicates_IsLazy() + { + _ = new BreakingSequence().Duplicates(); + } + + [Test] + public void Streams_Duplicates_As_They_Are_Discovered() + { + static IEnumerable Source() + { + yield return "DUPLICATED_STRING"; + yield return "DUPLICATED_STRING"; + throw new TestException(); + } + + using var source = Source().AsTestingSequence(); + + var results = source.Duplicates().Take(1).ToArray(); + + Assert.That(results, Is.EqualTo(new[] { "DUPLICATED_STRING" })); + } + + [Test] + public void Sequence_Without_Duplicates_Returns_Empty_Sequence() + { + using var input = TestingSequence.Of("FirstElement", "SecondElement", "ThirdElement"); + + var results = input.Duplicates().ToArray(); + + Assert.That(results, Is.Empty); + } + + [Test] + public void Sequence_With_Duplicates_Returns_Duplicates() + { + using var input = + TestingSequence.Of("FirstElement", + "DUPLICATED_STRING", + "DUPLICATED_STRING", + "DUPLICATED_STRING", + "ThirdElement"); + + var results = input.Duplicates().ToArray(); + + Assert.That(results, Contains.Item("DUPLICATED_STRING")); + Assert.That(results, Has.Exactly(1).Items); + } + + [Test] + public void Sequence_With_Multiple_Duplicates_Returns_One_Instance_Of_Each_Duplicates() + { + using var input = + TestingSequence.Of("FirstElement", + "DUPLICATED_STRING", + "DUPLICATED_STRING", + "DUPLICATED_STRING", + "ThirdElement", + "SECOND_DUPLICATED_STRING", + "SECOND_DUPLICATED_STRING"); + + var results = input.Duplicates().ToArray(); + + Assert.That(results, Contains.Item("DUPLICATED_STRING")); + Assert.That(results, Contains.Item("SECOND_DUPLICATED_STRING")); + Assert.That(results, Has.Exactly(2).Items); + } + + [Test] + public void Sequence_With_Duplicates_But_Using_Comparer_That_Always_Return_False_Returns_Empty_Sequence() + { + using var input = TestingSequence.Of("DUPLICATED_STRING", "DUPLICATED_STRING", "DUPLICATED_STRING"); + + var results = input.Duplicates(Delegate.EqualityComparer((_, _) => false, (string _) => 0)); + + Assert.That(results, Is.Empty); + } + } +} diff --git a/MoreLinq/Duplicates.cs b/MoreLinq/Duplicates.cs new file mode 100644 index 000000000..b29f9a65d --- /dev/null +++ b/MoreLinq/Duplicates.cs @@ -0,0 +1,64 @@ +#region License and Terms +// MoreLINQ - Extensions to LINQ to Objects +// Copyright (c) 2023 Julien Aspirot. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +namespace MoreLinq +{ + using System; + using System.Collections.Generic; + using System.Linq; + + static partial class MoreEnumerable + { + /// + /// Returns all duplicate elements of the given source. + /// + /// The source sequence. + /// The type of the elements in the source sequence. + /// is . + /// All elements that are duplicated. + /// This operator uses deferred execution and streams its results. + + public static IEnumerable Duplicates(this IEnumerable source) => + Duplicates(source, null); + + /// + /// Returns all duplicate elements of the given source, using the specified equality + /// comparer. + /// + /// The source sequence. + /// + /// The equality comparer to use to determine whether one + /// equals another. If , the default equality comparer for + /// is used. + /// The type of the elements in the source sequence. + /// is . + /// All elements that are duplicated. + /// This operator uses deferred execution and streams its results. + + public static IEnumerable Duplicates(this IEnumerable source, IEqualityComparer? comparer) + { + if (source is null) throw new ArgumentNullException(nameof(source)); + + return from e in source.ScanBy(static e => e, + static _ => 0, + static (count, _, _) => unchecked(Math.Min(count + 1, 3)), + comparer) + where e.Value is 2 + select e.Key; + } + } +} diff --git a/MoreLinq/Extensions.g.cs b/MoreLinq/Extensions.g.cs index b7fcf096d..83b2e90ac 100644 --- a/MoreLinq/Extensions.g.cs +++ b/MoreLinq/Extensions.g.cs @@ -1283,6 +1283,41 @@ public static IEnumerable DistinctBy(this IEnumerableDuplicates extension. + + [GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")] + public static partial class DuplicatesExtension + { + /// + /// Returns all duplicate elements of the given source. + /// + /// The source sequence. + /// The type of the elements in the source sequence. + /// is . + /// All elements that are duplicated. + /// This operator uses deferred execution and streams its results. + + public static IEnumerable Duplicates(this IEnumerable source) => MoreEnumerable.Duplicates(source); + + /// + /// Returns all duplicate elements of the given source, using the specified equality + /// comparer. + /// + /// The source sequence. + /// + /// The equality comparer to use to determine whether one + /// equals another. If , the default equality comparer for + /// is used. + /// The type of the elements in the source sequence. + /// is . + /// All elements that are duplicated. + /// This operator uses deferred execution and streams its results. + + public static IEnumerable Duplicates(this IEnumerable source, IEqualityComparer? comparer) + => MoreEnumerable.Duplicates(source, comparer); + + } + /// EndsWith extension. [GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")] diff --git a/MoreLinq/MoreLinq.csproj b/MoreLinq/MoreLinq.csproj index 6671f4934..38d48eee5 100644 --- a/MoreLinq/MoreLinq.csproj +++ b/MoreLinq/MoreLinq.csproj @@ -27,6 +27,7 @@ - CountDown - Consume - DistinctBy + - Duplicates - EndsWith - EquiZip - Evaluate diff --git a/MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt index 7dc5c5811..cada4690e 100644 --- a/MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +MoreLinq.Extensions.DuplicatesExtension +static MoreLinq.Extensions.DuplicatesExtension.Duplicates(this System.Collections.Generic.IEnumerable! source) -> System.Collections.Generic.IEnumerable! +static MoreLinq.Extensions.DuplicatesExtension.Duplicates(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEqualityComparer? comparer) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.Duplicates(this System.Collections.Generic.IEnumerable! source) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.Duplicates(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEqualityComparer? comparer) -> System.Collections.Generic.IEnumerable! diff --git a/MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt index ce7910980..057e63bee 100644 --- a/MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -30,6 +30,7 @@ MoreLinq.Extensions.CountBetweenExtension MoreLinq.Extensions.CountByExtension MoreLinq.Extensions.CountDownExtension MoreLinq.Extensions.DistinctByExtension +MoreLinq.Extensions.DuplicatesExtension MoreLinq.Extensions.EndsWithExtension MoreLinq.Extensions.EquiZipExtension MoreLinq.Extensions.EvaluateExtension @@ -183,6 +184,8 @@ static MoreLinq.Extensions.CountByExtension.CountBy(this System.C static MoreLinq.Extensions.CountDownExtension.CountDown(this System.Collections.Generic.IEnumerable! source, int count, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! static MoreLinq.Extensions.DistinctByExtension.DistinctBy(this System.Collections.Generic.IEnumerable! source, System.Func! keySelector) -> System.Collections.Generic.IEnumerable! static MoreLinq.Extensions.DistinctByExtension.DistinctBy(this System.Collections.Generic.IEnumerable! source, System.Func! keySelector, System.Collections.Generic.IEqualityComparer? comparer) -> System.Collections.Generic.IEnumerable! +static MoreLinq.Extensions.DuplicatesExtension.Duplicates(this System.Collections.Generic.IEnumerable! source) -> System.Collections.Generic.IEnumerable! +static MoreLinq.Extensions.DuplicatesExtension.Duplicates(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEqualityComparer? comparer) -> System.Collections.Generic.IEnumerable! static MoreLinq.Extensions.EndsWithExtension.EndsWith(this System.Collections.Generic.IEnumerable! first, System.Collections.Generic.IEnumerable! second) -> bool static MoreLinq.Extensions.EndsWithExtension.EndsWith(this System.Collections.Generic.IEnumerable! first, System.Collections.Generic.IEnumerable! second, System.Collections.Generic.IEqualityComparer? comparer) -> bool static MoreLinq.Extensions.EquiZipExtension.EquiZip(this System.Collections.Generic.IEnumerable! first, System.Collections.Generic.IEnumerable! second, System.Collections.Generic.IEnumerable! third, System.Collections.Generic.IEnumerable! fourth, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! @@ -441,6 +444,8 @@ static MoreLinq.MoreEnumerable.CountBy(this System.Collections.Ge static MoreLinq.MoreEnumerable.CountDown(this System.Collections.Generic.IEnumerable! source, int count, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! static MoreLinq.MoreEnumerable.DistinctBy(System.Collections.Generic.IEnumerable! source, System.Func! keySelector) -> System.Collections.Generic.IEnumerable! static MoreLinq.MoreEnumerable.DistinctBy(System.Collections.Generic.IEnumerable! source, System.Func! keySelector, System.Collections.Generic.IEqualityComparer? comparer) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.Duplicates(this System.Collections.Generic.IEnumerable! source) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.Duplicates(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEqualityComparer? comparer) -> System.Collections.Generic.IEnumerable! static MoreLinq.MoreEnumerable.EndsWith(this System.Collections.Generic.IEnumerable! first, System.Collections.Generic.IEnumerable! second) -> bool static MoreLinq.MoreEnumerable.EndsWith(this System.Collections.Generic.IEnumerable! first, System.Collections.Generic.IEnumerable! second, System.Collections.Generic.IEqualityComparer? comparer) -> bool static MoreLinq.MoreEnumerable.EquiZip(this System.Collections.Generic.IEnumerable! first, System.Collections.Generic.IEnumerable! second, System.Collections.Generic.IEnumerable! third, System.Collections.Generic.IEnumerable! fourth, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! diff --git a/MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 7dc5c5811..cada4690e 100644 --- a/MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +MoreLinq.Extensions.DuplicatesExtension +static MoreLinq.Extensions.DuplicatesExtension.Duplicates(this System.Collections.Generic.IEnumerable! source) -> System.Collections.Generic.IEnumerable! +static MoreLinq.Extensions.DuplicatesExtension.Duplicates(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEqualityComparer? comparer) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.Duplicates(this System.Collections.Generic.IEnumerable! source) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.Duplicates(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEqualityComparer? comparer) -> System.Collections.Generic.IEnumerable! diff --git a/MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt index 7dc5c5811..cada4690e 100644 --- a/MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt +++ b/MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +MoreLinq.Extensions.DuplicatesExtension +static MoreLinq.Extensions.DuplicatesExtension.Duplicates(this System.Collections.Generic.IEnumerable! source) -> System.Collections.Generic.IEnumerable! +static MoreLinq.Extensions.DuplicatesExtension.Duplicates(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEqualityComparer? comparer) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.Duplicates(this System.Collections.Generic.IEnumerable! source) -> System.Collections.Generic.IEnumerable! +static MoreLinq.MoreEnumerable.Duplicates(this System.Collections.Generic.IEnumerable! source, System.Collections.Generic.IEqualityComparer? comparer) -> System.Collections.Generic.IEnumerable! diff --git a/README.md b/README.md index 98d40de8a..2a897f211 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,12 @@ projected type. This method has 2 overloads. +### Duplicates + +Returns all duplicate elements of the given source. + +This method has 2 overloads. + ### EndsWith Determines whether the end of the first sequence is equivalent to the second diff --git a/bld/Copyright.props b/bld/Copyright.props index 62e21672d..96a381457 100644 --- a/bld/Copyright.props +++ b/bld/Copyright.props @@ -7,6 +7,7 @@ Portions © 2015 Felipe Sateler, “sholland”. Portions © 2016 Andreas Gullberg Larsen, Leandro F. Vieira (leandromoh). Portions © 2017 Jonas Nyrup (jnyrup). + Portions © 2023 Julien Aspirot (julienasp). Portions © Microsoft. All rights reserved.