Skip to content

Commit

Permalink
Add "Duplicates" extension
Browse files Browse the repository at this point in the history
This is a squashed merge of PR #1037 that partially addresses #125.

---------

Co-authored-by: Atif Aziz <code@raboof.com>
  • Loading branch information
julienasp and atifaziz authored Nov 19, 2023
1 parent 2024e82 commit 3a3c256
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 0 deletions.
105 changes: 105 additions & 0 deletions MoreLinq.Test/DuplicatesTest.cs
Original file line number Diff line number Diff line change
@@ -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<object>().Duplicates();
}

[Test]
public void Streams_Duplicates_As_They_Are_Discovered()
{
static IEnumerable<string> 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);
}
}
}
64 changes: 64 additions & 0 deletions MoreLinq/Duplicates.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Returns all duplicate elements of the given source.
/// </summary>
/// <param name="source">The source sequence.</param>
/// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
/// <returns>All elements that are duplicated.</returns>
/// <remarks>This operator uses deferred execution and streams its results.</remarks>

public static IEnumerable<TSource> Duplicates<TSource>(this IEnumerable<TSource> source) =>
Duplicates(source, null);

/// <summary>
/// Returns all duplicate elements of the given source, using the specified equality
/// comparer.
/// </summary>
/// <param name="source">The source sequence.</param>
/// <param name="comparer">
/// The equality comparer to use to determine whether one <typeparamref name="TSource"/>
/// equals another. If <see langword="null"/>, the default equality comparer for
/// <typeparamref name="TSource"/> is used.</param>
/// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
/// <returns>All elements that are duplicated.</returns>
/// <remarks>This operator uses deferred execution and streams its results.</remarks>

public static IEnumerable<TSource> Duplicates<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource>? 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;
}
}
}
35 changes: 35 additions & 0 deletions MoreLinq/Extensions.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,41 @@ public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TS

}

/// <summary><c>Duplicates</c> extension.</summary>

[GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")]
public static partial class DuplicatesExtension
{
/// <summary>
/// Returns all duplicate elements of the given source.
/// </summary>
/// <param name="source">The source sequence.</param>
/// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
/// <returns>All elements that are duplicated.</returns>
/// <remarks>This operator uses deferred execution and streams its results.</remarks>

public static IEnumerable<TSource> Duplicates<TSource>(this IEnumerable<TSource> source) => MoreEnumerable.Duplicates(source);

/// <summary>
/// Returns all duplicate elements of the given source, using the specified equality
/// comparer.
/// </summary>
/// <param name="source">The source sequence.</param>
/// <param name="comparer">
/// The equality comparer to use to determine whether one <typeparamref name="TSource"/>
/// equals another. If <see langword="null"/>, the default equality comparer for
/// <typeparamref name="TSource"/> is used.</param>
/// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
/// <returns>All elements that are duplicated.</returns>
/// <remarks>This operator uses deferred execution and streams its results.</remarks>

public static IEnumerable<TSource> Duplicates<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource>? comparer)
=> MoreEnumerable.Duplicates(source, comparer);

}

/// <summary><c>EndsWith</c> extension.</summary>

[GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")]
Expand Down
1 change: 1 addition & 0 deletions MoreLinq/MoreLinq.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- CountDown
- Consume
- DistinctBy
- Duplicates
- EndsWith
- EquiZip
- Evaluate
Expand Down
5 changes: 5 additions & 0 deletions MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
MoreLinq.Extensions.DuplicatesExtension
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
5 changes: 5 additions & 0 deletions MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -183,6 +184,8 @@ static MoreLinq.Extensions.CountByExtension.CountBy<TSource, TKey>(this System.C
static MoreLinq.Extensions.CountDownExtension.CountDown<T, TResult>(this System.Collections.Generic.IEnumerable<T>! source, int count, System.Func<T, int?, TResult>! resultSelector) -> System.Collections.Generic.IEnumerable<TResult>!
static MoreLinq.Extensions.DistinctByExtension.DistinctBy<TSource, TKey>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Func<TSource, TKey>! keySelector) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.DistinctByExtension.DistinctBy<TSource, TKey>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Func<TSource, TKey>! keySelector, System.Collections.Generic.IEqualityComparer<TKey>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.EndsWithExtension.EndsWith<T>(this System.Collections.Generic.IEnumerable<T>! first, System.Collections.Generic.IEnumerable<T>! second) -> bool
static MoreLinq.Extensions.EndsWithExtension.EndsWith<T>(this System.Collections.Generic.IEnumerable<T>! first, System.Collections.Generic.IEnumerable<T>! second, System.Collections.Generic.IEqualityComparer<T>? comparer) -> bool
static MoreLinq.Extensions.EquiZipExtension.EquiZip<T1, T2, T3, T4, TResult>(this System.Collections.Generic.IEnumerable<T1>! first, System.Collections.Generic.IEnumerable<T2>! second, System.Collections.Generic.IEnumerable<T3>! third, System.Collections.Generic.IEnumerable<T4>! fourth, System.Func<T1, T2, T3, T4, TResult>! resultSelector) -> System.Collections.Generic.IEnumerable<TResult>!
Expand Down Expand Up @@ -441,6 +444,8 @@ static MoreLinq.MoreEnumerable.CountBy<TSource, TKey>(this System.Collections.Ge
static MoreLinq.MoreEnumerable.CountDown<T, TResult>(this System.Collections.Generic.IEnumerable<T>! source, int count, System.Func<T, int?, TResult>! resultSelector) -> System.Collections.Generic.IEnumerable<TResult>!
static MoreLinq.MoreEnumerable.DistinctBy<TSource, TKey>(System.Collections.Generic.IEnumerable<TSource>! source, System.Func<TSource, TKey>! keySelector) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.DistinctBy<TSource, TKey>(System.Collections.Generic.IEnumerable<TSource>! source, System.Func<TSource, TKey>! keySelector, System.Collections.Generic.IEqualityComparer<TKey>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.EndsWith<T>(this System.Collections.Generic.IEnumerable<T>! first, System.Collections.Generic.IEnumerable<T>! second) -> bool
static MoreLinq.MoreEnumerable.EndsWith<T>(this System.Collections.Generic.IEnumerable<T>! first, System.Collections.Generic.IEnumerable<T>! second, System.Collections.Generic.IEqualityComparer<T>? comparer) -> bool
static MoreLinq.MoreEnumerable.EquiZip<T1, T2, T3, T4, TResult>(this System.Collections.Generic.IEnumerable<T1>! first, System.Collections.Generic.IEnumerable<T2>! second, System.Collections.Generic.IEnumerable<T3>! third, System.Collections.Generic.IEnumerable<T4>! fourth, System.Func<T1, T2, T3, T4, TResult>! resultSelector) -> System.Collections.Generic.IEnumerable<TResult>!
Expand Down
5 changes: 5 additions & 0 deletions MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
MoreLinq.Extensions.DuplicatesExtension
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
5 changes: 5 additions & 0 deletions MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
MoreLinq.Extensions.DuplicatesExtension
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions bld/Copyright.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Portions &#169; 2015 Felipe Sateler, &#8220;sholland&#8221;.
Portions &#169; 2016 Andreas Gullberg Larsen, Leandro F. Vieira (leandromoh).
Portions &#169; 2017 Jonas Nyrup (jnyrup).
Portions &#169; 2023 Julien Aspirot (julienasp).
Portions &#169; Microsoft. All rights reserved.
</Copyright>
</PropertyGroup>
Expand Down

0 comments on commit 3a3c256

Please sign in to comment.