Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MinBy/MaxBy Null Handling #241

Merged
merged 3 commits into from
Oct 25, 2024
Merged
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
7 changes: 3 additions & 4 deletions src/Polyfill/Polyfill_IEnumerable_Max.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ static partial class Polyfill
/// </remarks>
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.max?view=net-8.0#system-linq-enumerable-max-1(system-collections-generic-ienumerable((-0))-system-collections-generic-icomparer((-0)))")]
public static TSource? Max<TSource>(
this IEnumerable<TSource> target,
this IEnumerable<TSource> source,
IComparer<TSource>? comparer) =>
target
.OrderByDescending(_ => _, comparer)
.FirstOrDefault();
source
.MaxBy(_ => _, comparer);

#endif
}
87 changes: 78 additions & 9 deletions src/Polyfill/Polyfill_IEnumerable_MaxBy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ static partial class Polyfill
/// <summary>
/// Returns the maximum value in a generic sequence according to a specified key selector function.
/// </summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="target" />.</typeparam>
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
/// <typeparam name="TKey">The type of key to compare elements by.</typeparam>
/// <param name="source">A sequence of values to determine the maximum value of.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
Expand All @@ -27,12 +27,12 @@ static partial class Polyfill
/// </remarks>
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby#system-linq-enumerable-maxby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1)))")]
public static TSource? MaxBy<TSource, TKey>(
this IEnumerable<TSource> target,
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector) =>
MaxBy(target, keySelector, null);
MaxBy(source, keySelector, null);

/// <summary>Returns the maximum value in a generic sequence according to a specified key selector function.</summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="target" />.</typeparam>
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
/// <typeparam name="TKey">The type of key to compare elements by.</typeparam>
/// <param name="source">A sequence of values to determine the maximum value of.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
Expand All @@ -45,12 +45,81 @@ static partial class Polyfill
/// </remarks>
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby#system-linq-enumerable-maxby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-collections-generic-icomparer((-1)))")]
public static TSource? MaxBy<TSource, TKey>(
this IEnumerable<TSource> target,
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IComparer<TKey>? comparer) =>
target
.OrderByDescending(keySelector, comparer)
.FirstOrDefault();
IComparer<TKey>? comparer)
{
// Simplified from https://github.com/dotnet/runtime/blob/5d09a8f94c72ca4ef0a9c79eb9c58d06198e3ba9/src/libraries/System.Linq/src/System/Linq/Max.cs#L445-L526
if (source is null) throw new ArgumentNullException(nameof(source));
if (keySelector is null) throw new ArgumentNullException(nameof(keySelector));

comparer ??= Comparer<TKey>.Default;

using IEnumerator<TSource> e = source.GetEnumerator();

if (!e.MoveNext())
{
if (default(TSource) is null)
{
return default;
}
else
{
ThrowHelper.ThrowNoElementsException();
}
}

TSource value = e.Current;
TKey key = keySelector(value);

if (default(TKey) is null)
{
if (key is null)
{
TSource firstValue = value;

do
{
if (!e.MoveNext())
{
// All keys are null, surface the first element.
return firstValue;
}

value = e.Current;
key = keySelector(value);
}
while (key is null);
}

while (e.MoveNext())
{
TSource nextValue = e.Current;
TKey nextKey = keySelector(nextValue);
if (nextKey is not null && comparer.Compare(nextKey, key) > 0)
{
key = nextKey;
value = nextValue;
}
}
}
else
{
while (e.MoveNext())
{
TSource nextValue = e.Current;
TKey nextKey = keySelector(nextValue);
if (comparer.Compare(nextKey, key) > 0)
{
key = nextKey;
value = nextValue;
}
}
}

return value;

}

#endif
}
7 changes: 3 additions & 4 deletions src/Polyfill/Polyfill_IEnumerable_Min.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,10 @@ static partial class Polyfill
/// </remarks>
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.min?view=net-8.0#system-linq-enumerable-min-1(system-collections-generic-ienumerable((-0))-system-collections-generic-icomparer((-0)))")]
public static TSource? Min<TSource>(
this IEnumerable<TSource> target,
this IEnumerable<TSource> source,
IComparer<TSource>? comparer) =>
target
.OrderBy(_ => _, comparer)
.FirstOrDefault();
source
.MinBy(_ => _, comparer);

#endif

Expand Down
88 changes: 78 additions & 10 deletions src/Polyfill/Polyfill_IEnumerable_MinBy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ static partial class Polyfill
/// <summary>
/// Returns the minimum value in a generic sequence according to a specified key selector function.
/// </summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="target" />.</typeparam>
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
/// <typeparam name="TKey">The type of key to compare elements by.</typeparam>
/// <param name="source">A sequence of values to determine the minby value of.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
Expand All @@ -28,30 +28,98 @@ static partial class Polyfill
/// </remarks>
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.minby#system-linq-enumerable-minby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1)))")]
public static TSource? MinBy<TSource, TKey>(
this IEnumerable<TSource> target,
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector) =>
MinBy(target, keySelector, null);
MinBy(source, keySelector, null);

/// <summary>Returns the minimum value in a generic sequence according to a specified key selector function.</summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="target" />.</typeparam>
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
/// <typeparam name="TKey">The type of key to compare elements by.</typeparam>
/// <param name="source">A sequence of values to determine the minimum value of.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
/// <param name="comparer">The <see cref="IComparer{TKey}" /> to compare keys.</param>
/// <returns>The value with the minimum key in the sequence.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException">No key extracted from <paramref name="target" /> implements the <see cref="IComparable" /> or <see cref="IComparable{TKey}" /> interface.</exception>
/// <exception cref="ArgumentException">No key extracted from <paramref name="source" /> implements the <see cref="IComparable" /> or <see cref="IComparable{TKey}" /> interface.</exception>
/// <remarks>
/// <para>If <typeparamref name="TKey" /> is a reference type and the source sequence is empty or contains only values that are <see langword="null" />, this method returns <see langword="null" />.</para>
/// </remarks>
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.minby#system-linq-enumerable-minby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-collections-generic-icomparer((-1)))")]
public static TSource? MinBy<TSource, TKey>(
this IEnumerable<TSource> target,
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IComparer<TKey>? comparer) =>
target
.OrderBy(keySelector, comparer)
.FirstOrDefault();
IComparer<TKey>? comparer)
{
// Simplified from https://github.com/dotnet/runtime/blob/5d09a8f94c72ca4ef0a9c79eb9c58d06198e3ba9/src/libraries/System.Linq/src/System/Linq/Min.cs#L413-L503
if (source is null) throw new ArgumentNullException(nameof(source));
if (keySelector is null) throw new ArgumentNullException(nameof(keySelector));

comparer ??= Comparer<TKey>.Default;

using IEnumerator<TSource> e = source.GetEnumerator();

if (!e.MoveNext())
{
if (default(TSource) is null)
{
return default;
}
else
{
ThrowHelper.ThrowNoElementsException();
}
}

TSource value = e.Current;
TKey key = keySelector(value);

if (default(TKey) is null)
{
if (key is null)
{
TSource firstValue = value;

do
{
if (!e.MoveNext())
{
// All keys are null, surface the first element.
return firstValue;
}

value = e.Current;
key = keySelector(value);
}
while (key is null);
}

while (e.MoveNext())
{
TSource nextValue = e.Current;
TKey nextKey = keySelector(nextValue);
if (nextKey is not null && comparer.Compare(nextKey, key) < 0)
{
key = nextKey;
value = nextValue;
}
}
}
else
{
while (e.MoveNext())
{
TSource nextValue = e.Current;
TKey nextKey = keySelector(nextValue);
if (comparer.Compare(nextKey, key) < 0)
{
key = nextKey;
value = nextValue;
}
}
}

return value;
}

#endif

Expand Down
13 changes: 13 additions & 0 deletions src/Polyfill/Polyfill_IEnumerable_ThrowHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// <auto-generated />
#pragma warning disable

namespace Polyfills;

static partial class Polyfill
{
private static class ThrowHelper
{
public static void ThrowNoElementsException() =>
throw new System.InvalidOperationException("Sequence contains no elements");
}
}
54 changes: 0 additions & 54 deletions src/Tests/PolyfillTests_IEnumerable.cs
Original file line number Diff line number Diff line change
@@ -1,52 +1,6 @@
// ReSharper disable ReturnValueOfPureMethodIsNotUsed
partial class PolyfillTests
{
[Test]
public void MaxBy()
{
IEnumerable<int> enumerable = [1, 2];

Assert.AreEqual(2, enumerable.MaxBy(_ => _));
}

[Test]
public void MaxComparer()
{
IEnumerable<int> enumerable = [1, 2];

Assert.AreEqual(1, enumerable.Max(new ReverseComparer()));
}

[Test]
public void MaxByComparer()
{
IEnumerable<int> enumerable = [1, 2];

Assert.AreEqual(1, enumerable.MaxBy(_ => _, new ReverseComparer()));
}

[Test]
public void MinComparer()
{
IEnumerable<int> enumerable = [1, 2];

Assert.AreEqual(2, enumerable.Min(new ReverseComparer()));
}

[Test]
public void MinByComparer()
{
IEnumerable<int> enumerable = [1, 2];

Assert.AreEqual(2, enumerable.MinBy(_ => _, new ReverseComparer()));
}

class ReverseComparer : IComparer<int>
{
public int Compare(int x, int y) =>
y.CompareTo(x);
}

[Test]
public void TakeRange()
{
Expand Down Expand Up @@ -86,14 +40,6 @@ public void Except()
Assert.AreEqual(1, enumerable.Except(2).Single());
}

[Test]
public void MinBy()
{
IEnumerable<int> enumerable = new List<int> {1, 2};

Assert.AreEqual(1, enumerable.MinBy(_ => _));
}

[Test]
public void TryGetNonEnumeratedCount()
{
Expand Down
Loading