Skip to content

Commit

Permalink
MinBy/MaxBy Null Handling (#241)
Browse files Browse the repository at this point in the history
* More Min/Max tests

* Param is source

* Fix min/max null handling
  • Loading branch information
dahlbyk authored Oct 25, 2024
1 parent 13feaad commit bf7853e
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 81 deletions.
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

0 comments on commit bf7853e

Please sign in to comment.