From baff8f3f4089af04af431e7e395860bf6e106561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Andr=C3=A9?= <2341261+manandre@users.noreply.github.com> Date: Sun, 17 May 2026 21:29:54 +0000 Subject: [PATCH] Add MemoryExtensions.Min/Max --- .../System.Memory/ref/System.Memory.cs | 4 + .../tests/ReadOnlySpan/MinMax.cs | 86 ++++++++++++ .../tests/System.Memory.Tests.csproj | 1 + .../src/System/MemoryExtensions.cs | 126 ++++++++++++++++++ 4 files changed, 217 insertions(+) create mode 100644 src/libraries/System.Memory/tests/ReadOnlySpan/MinMax.cs diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index 7d53b7a8da7630..9642b313123609 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -230,6 +230,10 @@ public static partial class MemoryExtensions public static int BinarySearch(this System.Span span, T value, TComparer comparer) where TComparer : System.Collections.Generic.IComparer, allows ref struct { throw null; } [System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute(-1)] public static int BinarySearch(this System.Span span, TComparable comparable) where TComparable : System.IComparable, allows ref struct { throw null; } + public static T? Max(this System.ReadOnlySpan span) { throw null; } + public static T? Max(this System.ReadOnlySpan span, System.Collections.Generic.IComparer comparer) { throw null; } + public static T? Min(this System.ReadOnlySpan span) { throw null; } + public static T? Min(this System.ReadOnlySpan span, System.Collections.Generic.IComparer comparer) { throw null; } [System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute(-1)] public static int CommonPrefixLength(this System.Span span, System.ReadOnlySpan other) { throw null; } [System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute(-1)] diff --git a/src/libraries/System.Memory/tests/ReadOnlySpan/MinMax.cs b/src/libraries/System.Memory/tests/ReadOnlySpan/MinMax.cs new file mode 100644 index 00000000000000..c3cf844d904df3 --- /dev/null +++ b/src/libraries/System.Memory/tests/ReadOnlySpan/MinMax.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace System.SpanTests +{ + public static partial class ReadOnlySpanTests + { + [Fact] + public static void MinMax_Empty_NonNullableValueType_Throws() + { + ReadOnlySpan span = ReadOnlySpan.Empty; + + TestHelpers.AssertThrows(span, (_span) => _span.Min()); + TestHelpers.AssertThrows(span, (_span) => _span.Max()); + TestHelpers.AssertThrows(span, (_span) => _span.Min(Comparer.Default)); + TestHelpers.AssertThrows(span, (_span) => _span.Max(Comparer.Default)); + } + + [Fact] + public static void MinMax_Empty_ReferenceAndNullableValueType_ReturnsNull() + { + ReadOnlySpan strings = ReadOnlySpan.Empty; + ReadOnlySpan nullableInts = ReadOnlySpan.Empty; + + Assert.Null(strings.Min()); + Assert.Null(strings.Max()); + Assert.Null(nullableInts.Min()); + Assert.Null(nullableInts.Max()); + + Assert.Null(strings.Min(comparer: null!)); + Assert.Null(strings.Max(comparer: null!)); + Assert.Null(nullableInts.Min(comparer: null!)); + Assert.Null(nullableInts.Max(comparer: null!)); + } + + [Fact] + public static void MinMax_AllNull_ReturnsNull() + { + ReadOnlySpan strings = new string?[] { null, null, null }; + ReadOnlySpan nullableInts = new int?[] { null, null, null }; + + Assert.Null(strings.Min()); + Assert.Null(strings.Max()); + Assert.Null(nullableInts.Min()); + Assert.Null(nullableInts.Max()); + } + + [Fact] + public static void MinMax_NullNotFirst_NullIgnoredForComparison() + { + ReadOnlySpan strings = new string?[] { "charlie", null, "bravo", null, "delta" }; + ReadOnlySpan nullableInts = new int?[] { 4, null, -1, null, 7 }; + + Assert.Equal("bravo", strings.Min()); + Assert.Equal("delta", strings.Max()); + Assert.Equal(-1, nullableInts.Min()); + Assert.Equal(7, nullableInts.Max()); + } + + [Fact] + public static void MinMax_DefaultComparer_ProducesExpectedValues() + { + ReadOnlySpan ints = new int[] { 4, -1, 7, 2 }; + ReadOnlySpan strings = new string?[] { null, "charlie", "bravo", null, "delta" }; + + Assert.Equal(-1, ints.Min()); + Assert.Equal(7, ints.Max()); + + Assert.Equal("bravo", strings.Min()); + Assert.Equal("delta", strings.Max()); + } + + [Fact] + public static void MinMax_CustomComparer_IsUsed() + { + ReadOnlySpan ints = new int[] { 4, -1, 7, 2 }; + IComparer reverse = Comparer.Create((left, right) => right.CompareTo(left)); + + Assert.Equal(7, ints.Min(reverse)); + Assert.Equal(-1, ints.Max(reverse)); + } + } +} diff --git a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj index 5ef352b8d1f6d9..33e76aacc5e7c7 100644 --- a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj +++ b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj @@ -30,6 +30,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs index 2ef440179364ef..4bbf319ab85b07 100644 --- a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs @@ -4312,6 +4312,132 @@ public static int BinarySearch( return BinarySearch(span, comparable); } + /// + /// Returns the minimum value in the span. + /// + /// The type of the elements in the span. + /// The span of values to determine the minimum value of. + /// The minimum value in the span. + /// is empty and is a non-nullable value type. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T? Min(this ReadOnlySpan span) => + Min(span, comparer: null!); + + /// + /// Returns the minimum value in the span. + /// + /// The type of the elements in the span. + /// The span of values to determine the minimum value of. + /// The to compare values. + /// The minimum value in the span. + /// is empty and is a non-nullable value type. + public static T? Min(this ReadOnlySpan span, IComparer comparer) + { + if (span.IsEmpty) + { + if (default(T) is null) + { + return default; + } + + ThrowHelper.ThrowInvalidOperationException(); + } + + return comparer is null || comparer == Comparer.Default + ? MinCore(span, Comparer.Default) + : MinCore(span, comparer); + + static T? MinCore(ReadOnlySpan span, TComparer comparer) + where TComparer : IComparer + { + T? value = span[0]; + int i = 1; + + while (value is null) + { + if ((uint)i >= (uint)span.Length) + { + return value; + } + value = span[i++]; + } + + for (; (uint)i < (uint)span.Length; i++) + { + T next = span[i]; + if (next is not null && comparer.Compare(next, value) < 0) + { + value = next; + } + } + + return value; + } + } + + /// + /// Returns the maximum value in the span. + /// + /// The type of the elements in the span. + /// The span of values to determine the maximum value of. + /// The maximum value in the span. + /// is empty and is a non-nullable value type. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T? Max(this ReadOnlySpan span) => + Max(span, comparer: null!); + + /// + /// Returns the maximum value in the span. + /// + /// The type of the elements in the span. + /// The span of values to determine the maximum value of. + /// The to compare values. + /// The maximum value in the span. + /// is empty and is a non-nullable value type. + public static T? Max(this ReadOnlySpan span, IComparer comparer) + { + if (span.IsEmpty) + { + if (default(T) is null) + { + return default; + } + + ThrowHelper.ThrowInvalidOperationException(); + } + + return comparer is null || comparer == Comparer.Default + ? MaxCore(span, Comparer.Default) + : MaxCore(span, comparer); + + static T? MaxCore(ReadOnlySpan span, TComparer comparer) + where TComparer : IComparer + { + T? value = span[0]; + int i = 1; + + while (value is null) + { + if ((uint)i >= (uint)span.Length) + { + return value; + } + value = span[i++]; + } + + for (; (uint)i < (uint)span.Length; i++) + { + T next = span[i]; + if (next is not null && comparer.Compare(next, value) > 0) + { + value = next; + } + } + + return value; + } + } + /// /// Sorts the elements in the entire using the implementation /// of each element of the