Add MinMax and MinMaxBy (resolves #72)

This commit is contained in:
Oliver Booth 2023-03-26 17:03:40 +01:00
parent e00a673a04
commit 3b85419da3
No known key found for this signature in database
GPG Key ID: 20BEB9DC87961025
3 changed files with 559 additions and 0 deletions

View File

@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- X10D: Added `IEnumerable<T>.FirstWhereNotOrDefault(Func<T, bool>)`.
- X10D: Added `IEnumerable<T>.LastWhereNot(Func<T, bool>)`.
- X10D: Added `IEnumerable<T>.LastWhereNotOrDefault(Func<T, bool>)`.
- X10D: Added `IEnumerable<T>.MinMax()` and `IEnumerable<T>.MinMaxBy()`.
- X10D: Added `IEnumerable<T>.WhereNot(Func<T, bool>)`.
- X10D: Added `IEnumerable<T>.WhereNotNull()`.
- X10D: Added `IList<T>.RemoveRange(Range)`.

View File

@ -0,0 +1,159 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using X10D.Linq;
namespace X10D.Tests.Linq;
[TestClass]
public class EnumerableTests
{
[TestMethod]
public void MinMax_ShouldReturnCorrectValues_UsingDefaultComparer()
{
IEnumerable<int> source = Enumerable.Range(1, 10);
(int minimum, int maximum) = source.MinMax();
Assert.AreEqual(1, minimum);
Assert.AreEqual(10, maximum);
}
[TestMethod]
public void MinMax_ShouldReturnCorrectSelectedValues_UsingDefaultComparer()
{
IEnumerable<Person> source = Enumerable.Range(1, 10).Select(i => new Person {Age = i});
(int minimum, int maximum) = source.MinMax(p => p.Age);
Assert.AreEqual(1, minimum);
Assert.AreEqual(10, maximum);
}
[TestMethod]
public void MinMax_ShouldReturnOppositeSelectedValues_UsingInverseComparer()
{
IEnumerable<Person> source = Enumerable.Range(1, 10).Select(i => new Person {Age = i});
(int minimum, int maximum) = source.MinMax(p => p.Age, new InverseComparer<int>());
Assert.AreEqual(10, minimum);
Assert.AreEqual(1, maximum);
}
[TestMethod]
public void MinMax_ShouldReturnOppositeValues_UsingInverseComparer()
{
IEnumerable<int> source = Enumerable.Range(1, 10);
(int minimum, int maximum) = source.MinMax(new InverseComparer<int>());
Assert.AreEqual(10, minimum);
Assert.AreEqual(1, maximum);
}
[TestMethod]
public void MinMax_ShouldThrowArgumentNullException_GivenNullSelector()
{
IEnumerable<int> source = Enumerable.Range(1, 10);
Assert.ThrowsException<ArgumentNullException>(() => source.MinMax((Func<int, int>?)null!));
}
[TestMethod]
public void MinMax_ShouldThrowArgumentNullException_GivenNullSource()
{
IEnumerable<int>? source = null;
Assert.ThrowsException<ArgumentNullException>(() => source!.MinMax());
}
[TestMethod]
public void MinMax_ShouldThrowInvalidOperationException_GivenEmptySource()
{
IEnumerable<int> source = ArraySegment<int>.Empty;
Assert.ThrowsException<InvalidOperationException>(() => source.MinMax());
}
[TestMethod]
public void MinMaxBy_ShouldReturnCorrectSelectedValues_UsingDefaultComparer()
{
IEnumerable<Person> source = Enumerable.Range(1, 10).Select(i => new Person {Age = i});
(Person minimum, Person maximum) = source.MinMaxBy(p => p.Age);
Assert.AreEqual(1, minimum.Age);
Assert.AreEqual(10, maximum.Age);
}
[TestMethod]
public void MinMaxBy_ShouldReturnOppositeSelectedValues_UsingInverseComparer()
{
IEnumerable<Person> source = Enumerable.Range(1, 10).Select(i => new Person {Age = i});
(Person minimum, Person maximum) = source.MinMaxBy(p => p.Age, new InverseComparer<int>());
Assert.AreEqual(10, minimum.Age);
Assert.AreEqual(1, maximum.Age);
}
[TestMethod]
public void MinMaxBy_ShouldThrowArgumentNullException_GivenNullSelector()
{
IEnumerable<Person> source = Enumerable.Range(1, 10).Select(i => new Person {Age = i});
Assert.ThrowsException<ArgumentNullException>(() => source.MinMaxBy((Func<Person, int>?)null!));
}
[TestMethod]
public void MinMaxBy_ShouldThrowArgumentNullException_GivenNullSource()
{
IEnumerable<Person>? source = null;
Assert.ThrowsException<ArgumentNullException>(() => source!.MinMaxBy(p => p.Age));
}
[TestMethod]
public void MinMaxBy_ShouldThrowInvalidOperationException_GivenEmptySource()
{
IEnumerable<Person> source = ArraySegment<Person>.Empty;
Assert.ThrowsException<InvalidOperationException>(() => source.MinMaxBy(p => p.Age));
}
private struct InverseComparer<T> : IComparer<T> where T : IComparable<T>
{
public int Compare(T? x, T? y)
{
if (x is null)
{
return y is null ? 0 : 1;
}
return y is null ? -1 : y.CompareTo(x);
}
}
private struct Person : IComparable<Person>, IComparable
{
public int Age { get; set; }
public static bool operator <(Person left, Person right)
{
return left.CompareTo(right) < 0;
}
public static bool operator >(Person left, Person right)
{
return left.CompareTo(right) > 0;
}
public static bool operator <=(Person left, Person right)
{
return left.CompareTo(right) <= 0;
}
public static bool operator >=(Person left, Person right)
{
return left.CompareTo(right) >= 0;
}
public int CompareTo(Person other)
{
return Age.CompareTo(other.Age);
}
public int CompareTo(object? obj)
{
if (ReferenceEquals(null, obj))
{
return 1;
}
return obj is Person other
? CompareTo(other)
: throw new ArgumentException($"Object must be of type {nameof(Person)}");
}
}
}

View File

@ -0,0 +1,399 @@
using System.Runtime.InteropServices;
namespace X10D.Linq;
/// <summary>
/// LINQ-inspired extension methods for <see cref="IEnumerable{T}" />.
/// </summary>
public static class EnumerableExtensions
{
/// <summary>
/// Returns the minimum and maximum values in a sequence of values.
/// </summary>
/// <param name="source">A sequence of values to determine the minimum and maximum values of.</param>
/// <typeparam name="T">The type of the elements in <paramref name="source" />.</typeparam>
/// <returns>A tuple containing the minimum and maximum values in <paramref name="source" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="InvalidOperationException"><paramref name="source" /> contains no elements.</exception>
public static (T? Minimum, T? Maximum) MinMax<T>(this IEnumerable<T> source)
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(source);
#else
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
#endif
return MinMax(source, Comparer<T>.Default);
}
/// <summary>
/// Returns the minimum and maximum values in a sequence of values, using a specified comparer.
/// </summary>
/// <param name="source">A sequence of values to determine the minimum and maximum values of.</param>
/// <param name="comparer">The comparer which shall be used to compare each element in the sequence.</param>
/// <typeparam name="T">The type of the elements in <paramref name="source" />.</typeparam>
/// <returns>A tuple containing the minimum and maximum values in <paramref name="source" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="InvalidOperationException"><paramref name="source" /> contains no elements.</exception>
public static (T? Minimum, T? Maximum) MinMax<T>(this IEnumerable<T> source, IComparer<T>? comparer)
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(source);
#else
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
#endif
comparer ??= Comparer<T>.Default;
T? minValue;
T? maxValue;
// ReSharper disable once PossibleMultipleEnumeration
if (source.TryGetSpan(out ReadOnlySpan<T> span))
{
if (span.IsEmpty)
{
throw new InvalidOperationException("Source contains no elements");
}
minValue = span[0];
maxValue = minValue;
for (var index = 1; (uint)index < (uint)span.Length; index++)
{
T current = span[index];
if (comparer.Compare(current, minValue) < 0)
{
minValue = current;
}
if (comparer.Compare(current, maxValue) > 0)
{
maxValue = current;
}
}
return (minValue, maxValue);
}
// ReSharper disable once PossibleMultipleEnumeration
using (IEnumerator<T> enumerator = source.GetEnumerator())
{
if (!enumerator.MoveNext())
{
throw new InvalidOperationException("Source contains no elements");
}
minValue = enumerator.Current;
maxValue = minValue;
while (enumerator.MoveNext())
{
T current = enumerator.Current;
if (minValue is null || comparer.Compare(current, minValue) < 0)
{
minValue = current;
}
if (maxValue is null || comparer.Compare(current, maxValue) > 0)
{
maxValue = current;
}
}
}
return (minValue, maxValue);
}
/// <summary>
/// Invokes a transform function on each element of a sequence of elements and returns the minimum and maximum values.
/// </summary>
/// <param name="source">A sequence of values to determine the minimum and maximum values of.</param>
/// <param name="selector">A transform function to apply to each element.</param>
/// <typeparam name="TSource">The type of the elements in <paramref name="source" />.</typeparam>
/// <typeparam name="TResult">The type of the elements to compare.</typeparam>
/// <returns>A tuple containing the minimum and maximum values in <paramref name="source" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="InvalidOperationException"><paramref name="source" /> contains no elements.</exception>
public static (TResult? Minimum, TResult? Maximum) MinMax<TSource, TResult>(this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(selector);
#else
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
#endif
return MinMax(source, selector, Comparer<TResult>.Default);
}
/// <summary>
/// Invokes a transform function on each element of a sequence of elements and returns the minimum and maximum values,
/// using a specified comparer.
/// </summary>
/// <param name="source">A sequence of values to determine the minimum and maximum values of.</param>
/// <param name="selector">A transform function to apply to each element.</param>
/// <param name="comparer">The comparer which shall be used to compare each element in the sequence.</param>
/// <typeparam name="TSource">The type of the elements in <paramref name="source" />.</typeparam>
/// <typeparam name="TResult">The type of the elements to compare.</typeparam>
/// <returns>A tuple containing the minimum and maximum values in <paramref name="source" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="InvalidOperationException"><paramref name="source" /> contains no elements.</exception>
public static (TResult? Minimum, TResult? Maximum) MinMax<TSource, TResult>(this IEnumerable<TSource> source,
Func<TSource, TResult> selector,
IComparer<TResult>? comparer)
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(selector);
#else
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
#endif
comparer ??= Comparer<TResult>.Default;
TResult? minValue;
TResult? maxValue;
// ReSharper disable once PossibleMultipleEnumeration
if (source.TryGetSpan(out ReadOnlySpan<TSource> span))
{
if (span.IsEmpty)
{
throw new InvalidOperationException("Source contains no elements");
}
minValue = selector(span[0]);
maxValue = minValue;
for (var index = 1; (uint)index < (uint)span.Length; index++)
{
TResult current = selector(span[index]);
if (minValue is null || comparer.Compare(current, minValue) < 0)
{
minValue = current;
}
if (maxValue is null || comparer.Compare(current, maxValue) > 0)
{
maxValue = current;
}
}
return (minValue, maxValue);
}
// ReSharper disable once PossibleMultipleEnumeration
using (IEnumerator<TSource> enumerator = source.GetEnumerator())
{
if (!enumerator.MoveNext())
{
throw new InvalidOperationException("Source contains no elements");
}
minValue = selector(enumerator.Current);
maxValue = minValue;
while (enumerator.MoveNext())
{
TResult current = selector(enumerator.Current);
if (minValue is null || comparer.Compare(current, minValue) < 0)
{
minValue = current;
}
if (maxValue is null || comparer.Compare(current, maxValue) > 0)
{
maxValue = current;
}
}
}
return (minValue, maxValue);
}
/// <summary>
/// Returns the minimum and maximum values in a sequence according to a specified key selector function.
/// </summary>
/// <param name="source">A sequence of values to determine the minimum and maximum values of.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
/// <typeparam name="TSource">The type of the elements in <paramref name="source" />.</typeparam>
/// <typeparam name="TResult">The type of the elements to compare.</typeparam>
/// <returns>A tuple containing the minimum and maximum values in <paramref name="source" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="InvalidOperationException"><paramref name="source" /> contains no elements.</exception>
public static (TSource? Minimum, TSource? Maximum) MinMaxBy<TSource, TResult>(this IEnumerable<TSource> source,
Func<TSource, TResult> keySelector)
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(keySelector);
#else
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
if (keySelector is null)
{
throw new ArgumentNullException(nameof(keySelector));
}
#endif
return MinMaxBy(source, keySelector, Comparer<TResult>.Default);
}
/// <summary>
/// Returns the minimum and maximum values in a sequence according to a specified key selector function.
/// </summary>
/// <param name="source">A sequence of values to determine the minimum and maximum values of.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
/// <param name="comparer">The comparer which shall be used to compare each element in the sequence.</param>
/// <typeparam name="TSource">The type of the elements in <paramref name="source" />.</typeparam>
/// <typeparam name="TResult">The type of the elements to compare.</typeparam>
/// <returns>A tuple containing the minimum and maximum values in <paramref name="source" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="InvalidOperationException"><paramref name="source" /> contains no elements.</exception>
public static (TSource? Minimum, TSource? Maximum) MinMaxBy<TSource, TResult>(this IEnumerable<TSource> source,
Func<TSource, TResult> keySelector,
IComparer<TResult>? comparer)
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(keySelector);
#else
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
if (keySelector is null)
{
throw new ArgumentNullException(nameof(keySelector));
}
#endif
comparer ??= Comparer<TResult>.Default;
TSource? minValue;
TSource? maxValue;
// ReSharper disable once PossibleMultipleEnumeration
if (source.TryGetSpan(out ReadOnlySpan<TSource> span))
{
if (span.IsEmpty)
{
throw new InvalidOperationException("Source contains no elements");
}
minValue = span[0];
maxValue = minValue;
for (var index = 1; (uint)index < (uint)span.Length; index++)
{
TSource current = span[index];
TResult transformedCurrent = keySelector(current);
if (minValue is null || comparer.Compare(transformedCurrent, keySelector(minValue)) < 0)
{
minValue = current;
}
if (maxValue is null || comparer.Compare(transformedCurrent, keySelector(maxValue)) > 0)
{
maxValue = current;
}
}
return (minValue, maxValue);
}
// ReSharper disable once PossibleMultipleEnumeration
using (IEnumerator<TSource> enumerator = source.GetEnumerator())
{
if (!enumerator.MoveNext())
{
throw new InvalidOperationException("Source contains no elements");
}
minValue = enumerator.Current;
maxValue = minValue;
while (enumerator.MoveNext())
{
TSource current = enumerator.Current;
TResult transformedCurrent = keySelector(current);
if (minValue is null || comparer.Compare(transformedCurrent, keySelector(minValue)) < 0)
{
minValue = current;
}
if (maxValue is null || comparer.Compare(transformedCurrent, keySelector(maxValue)) > 0)
{
maxValue = current;
}
}
}
return (minValue, maxValue);
}
private static bool TryGetSpan<TSource>(this IEnumerable<TSource> source, out ReadOnlySpan<TSource> span)
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(source);
#else
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
#endif
var result = true;
switch (source)
{
case TSource[] array:
span = array;
break;
#if NET5_0_OR_GREATER
case List<TSource> list:
span = CollectionsMarshal.AsSpan(list);
break;
#endif
default:
span = default;
result = false;
break;
}
return result;
}
}