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..107e0736cb6e88 --- /dev/null +++ b/src/libraries/System.Memory/tests/ReadOnlySpan/MinMax.cs @@ -0,0 +1,92 @@ +// 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_NullComparer_ThrowsArgumentNullException() + { + ReadOnlySpan span = new int[] { 4, -1, 7, 2 }; + ReadOnlySpan emptySpan = ReadOnlySpan.Empty; + TestHelpers.AssertThrows(span, (_span) => _span.Min(comparer: null!)); + TestHelpers.AssertThrows(span, (_span) => _span.Max(comparer: null!)); + TestHelpers.AssertThrows(emptySpan, (_span) => _span.Min(comparer: null!)); + TestHelpers.AssertThrows(emptySpan, (_span) => _span.Max(comparer: null!)); + } + + [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()); + } + + [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/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index ac51112ce4dbd1..38ae696ec23048 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -4486,4 +4486,7 @@ The number styles AllowHexSpecifier and AllowBinarySpecifier are not supported on the decimal type. + + Sequence contains no elements. + diff --git a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs index 2ef440179364ef..4d03c7d1a2eead 100644 --- a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs @@ -4312,6 +4312,142 @@ 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. + /// + /// If type implements , the method uses that implementation to compare values. Otherwise, if type implements , that implementation is used to compare values. + /// If is a reference type and the span sequence is empty, this method returns . + /// Null values are ignored when determining the minimum value. If the span contains at least one non-null value, the minimum of those values is returned. If the span does not contain any non-null values, is returned. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T? Min(this ReadOnlySpan span) => + Min(span, Comparer.Default); + + /// + /// 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 . + /// is empty and is a non-nullable value type. + /// + /// If is a reference type and the span sequence is empty, this method returns . + /// Null values are ignored when determining the minimum value. If the span contains at least one non-null value, the minimum of those values is returned. If the span does not contain any non-null values, is returned. + /// + public static T? Min(this ReadOnlySpan span, IComparer comparer) + { + if (comparer is null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.comparer); + + if (span.IsEmpty) + { + if (default(T) is null) + { + return default; + } + + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_NoElements); + } + + 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. + /// + /// If type implements , the method uses that implementation to compare values. Otherwise, if type implements , that implementation is used to compare values. + /// If is a reference type and the span sequence is empty, this method returns . + /// Null values are ignored when determining the maximum value. If the span contains at least one non-null value, the maximum of those values is returned. If the span does not contain any non-null values, is returned. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T? Max(this ReadOnlySpan span) => + Max(span, Comparer.Default); + + /// + /// 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 . + /// is empty and is a non-nullable value type. + /// + /// If is a reference type and the span sequence is empty, this method returns . + /// Null values are ignored when determining the maximum value. If the span contains at least one non-null value, the maximum of those values is returned. If the span does not contain any non-null values, is returned. + /// + public static T? Max(this ReadOnlySpan span, IComparer comparer) + { + if (comparer is null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.comparer); + + if (span.IsEmpty) + { + if (default(T) is null) + { + return default; + } + + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_NoElements); + } + + 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 diff --git a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs index cf08add006be22..79b1e6e9aba57a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs @@ -1252,6 +1252,8 @@ private static string GetResourceString(ExceptionResource resource) return SR.ConcurrentDictionary_ItemKeyIsNull; case ExceptionResource.ConcurrentDictionary_TypeOfValueIncorrect: return SR.ConcurrentDictionary_TypeOfValueIncorrect; + case ExceptionResource.InvalidOperation_NoElements: + return SR.InvalidOperation_NoElements; default: Debug.Fail("The enum value is not defined, please check the ExceptionResource Enum."); return ""; @@ -1453,5 +1455,6 @@ internal enum ExceptionResource InvalidOperation_IncompatibleComparer, ConcurrentDictionary_ItemKeyIsNull, ConcurrentDictionary_TypeOfValueIncorrect, + InvalidOperation_NoElements, } }