diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index 726e1066e62a0f..18b6af79a9e9ed 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -91,6 +91,9 @@ public static void Sort(this System.Span span, System.Comparison compar public static void Sort(this System.Span keys, System.Span items) { } public static void Sort(this System.Span keys, System.Span items, TComparer comparer) where TComparer : System.Collections.Generic.IComparer? { } public static void Sort(this System.Span keys, System.Span items, System.Comparison comparison) { } + public static System.SpanSplitEnumerator Split(this System.ReadOnlySpan span) { throw null; } + public static System.SpanSplitEnumerator Split(this System.ReadOnlySpan span, char separator) { throw null; } + public static System.SpanSplitEnumerator Split(this System.ReadOnlySpan span, string separator) { throw null; } public static bool StartsWith(this System.ReadOnlySpan span, System.ReadOnlySpan value, System.StringComparison comparisonType) { throw null; } public static bool StartsWith(this System.ReadOnlySpan span, System.ReadOnlySpan value) where T : System.IEquatable { throw null; } public static bool StartsWith(this System.Span span, System.ReadOnlySpan value) where T : System.IEquatable { throw null; } @@ -156,6 +159,12 @@ public static void Sort(this System.Span keys, System.Span where T : System.IEquatable + { + public System.SpanSplitEnumerator GetEnumerator() { throw null; } + public readonly System.Range Current { get { throw null; } } + public bool MoveNext() { throw null; } + } } namespace System.Buffers { diff --git a/src/libraries/System.Memory/tests/ReadOnlySpan/Split.char.cs b/src/libraries/System.Memory/tests/ReadOnlySpan/Split.char.cs new file mode 100644 index 00000000000000..10a832270163f4 --- /dev/null +++ b/src/libraries/System.Memory/tests/ReadOnlySpan/Split.char.cs @@ -0,0 +1,260 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Xunit; + +namespace System.SpanTests +{ + public static partial class ReadOnlySpanTests + { + [Fact] + public static void SplitNoMatchSingleResult() + { + ReadOnlySpan value = "a b"; + + string expected = value.ToString(); + var enumerator = value.Split(','); + Assert.True(enumerator.MoveNext()); + Assert.Equal(expected, value[enumerator.Current].ToString()); + } + + [Fact] + public static void DefaultSpanSplitEnumeratorBehavior() + { + var charSpanEnumerator = new SpanSplitEnumerator(); + Assert.Equal(new Range(0, 0), charSpanEnumerator.Current); + Assert.False(charSpanEnumerator.MoveNext()); + + // Implicit DoesNotThrow assertion + charSpanEnumerator.GetEnumerator(); + + var stringSpanEnumerator = new SpanSplitEnumerator(); + Assert.Equal(new Range(0, 0), stringSpanEnumerator.Current); + Assert.False(stringSpanEnumerator.MoveNext()); + stringSpanEnumerator.GetEnumerator(); + } + + [Fact] + public static void ValidateArguments_OverloadWithoutSeparator() + { + ReadOnlySpan buffer = default; + + SpanSplitEnumerator enumerator = buffer.Split(); + Assert.True(enumerator.MoveNext()); + Assert.Equal(new Range(0, 0), enumerator.Current); + Assert.False(enumerator.MoveNext()); + + buffer = ""; + enumerator = buffer.Split(); + Assert.True(enumerator.MoveNext()); + Assert.Equal(new Range(0, 0), enumerator.Current); + Assert.False(enumerator.MoveNext()); + + buffer = " "; + enumerator = buffer.Split(); + Assert.True(enumerator.MoveNext()); + Assert.Equal(new Range(0, 0), enumerator.Current); + Assert.True(enumerator.MoveNext()); + Assert.Equal(new Range(1, 1), enumerator.Current); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public static void ValidateArguments_OverloadWithROSSeparator() + { + // Default buffer + ReadOnlySpan buffer = default; + + SpanSplitEnumerator enumerator = buffer.Split(default(char)); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.False(enumerator.MoveNext()); + + enumerator = buffer.Split(' '); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.False(enumerator.MoveNext()); + + // Empty buffer + buffer = ""; + + enumerator = buffer.Split(default(char)); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.False(enumerator.MoveNext()); + + enumerator = buffer.Split(' '); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.False(enumerator.MoveNext()); + + // Single whitespace buffer + buffer = " "; + + enumerator = buffer.Split(default(char)); + Assert.True(enumerator.MoveNext()); + Assert.False(enumerator.MoveNext()); + + enumerator = buffer.Split(' '); + Assert.Equal(new Range(0, 0), enumerator.Current); + Assert.True(enumerator.MoveNext()); + Assert.Equal(new Range(0, 0), enumerator.Current); + Assert.True(enumerator.MoveNext()); + Assert.Equal(new Range(1, 1), enumerator.Current); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public static void ValidateArguments_OverloadWithStringSeparator() + { + // Default buffer + ReadOnlySpan buffer = default; + + SpanSplitEnumerator enumerator = buffer.Split(null); // null is treated as empty string + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.False(enumerator.MoveNext()); + + enumerator = buffer.Split(""); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.False(enumerator.MoveNext()); + + enumerator = buffer.Split(" "); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.False(enumerator.MoveNext()); + + // Empty buffer + buffer = ""; + + enumerator = buffer.Split(null); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.False(enumerator.MoveNext()); + + enumerator = buffer.Split(""); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.False(enumerator.MoveNext()); + + enumerator = buffer.Split(" "); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.False(enumerator.MoveNext()); + + // Single whitespace buffer + buffer = " "; + + enumerator = buffer.Split(null); // null is treated as empty string + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(1, 1)); + Assert.False(enumerator.MoveNext()); + + enumerator = buffer.Split(""); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(1, 1)); + Assert.False(enumerator.MoveNext()); + + enumerator = buffer.Split(" "); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(0, 0)); + Assert.True(enumerator.MoveNext()); + Assert.Equal(enumerator.Current, new Range(1, 1)); + Assert.False(enumerator.MoveNext()); + } + + [Theory] + [InlineData("", ',', new[] { "" })] + [InlineData(" ", ' ', new[] { "", "" })] + [InlineData(",", ',', new[] { "", "" })] + [InlineData(" ", ' ', new[] { "", "", "", "", "", "" })] + [InlineData(",,", ',', new[] { "", "", "" })] + [InlineData("ab", ',', new[] { "ab" })] + [InlineData("a,b", ',', new[] { "a", "b" })] + [InlineData("a,", ',', new[] { "a", "" })] + [InlineData(",b", ',', new[] { "", "b" })] + [InlineData(",a,b", ',', new[] { "", "a", "b" })] + [InlineData("a,b,", ',', new[] { "a", "b", "" })] + [InlineData("a,b,c", ',', new[] { "a", "b", "c" })] + [InlineData("a,,c", ',', new[] { "a", "", "c" })] + [InlineData(",a,b,c", ',', new[] { "", "a", "b", "c" })] + [InlineData("a,b,c,", ',', new[] { "a", "b", "c", "" })] + [InlineData(",a,b,c,", ',', new[] { "", "a", "b", "c", "" })] + [InlineData("first,second", ',', new[] { "first", "second" })] + [InlineData("first,", ',', new[] { "first", "" })] + [InlineData(",second", ',', new[] { "", "second" })] + [InlineData(",first,second", ',', new[] { "", "first", "second" })] + [InlineData("first,second,", ',', new[] { "first", "second", "" })] + [InlineData("first,second,third", ',', new[] { "first", "second", "third" })] + [InlineData("first,,third", ',', new[] { "first", "", "third" })] + [InlineData(",first,second,third", ',', new[] { "", "first", "second", "third" })] + [InlineData("first,second,third,", ',', new[] { "first", "second", "third", "" })] + [InlineData(",first,second,third,", ',', new[] { "", "first", "second", "third", "" })] + [InlineData("Foo Bar Baz", ' ', new[] { "Foo", "Bar", "Baz" })] + [InlineData("Foo Bar Baz ", ' ', new[] { "Foo", "Bar", "Baz", "" })] + [InlineData(" Foo Bar Baz ", ' ', new[] { "", "Foo", "Bar", "Baz", "" })] + [InlineData(" Foo Bar Baz ", ' ', new[] { "", "Foo", "", "Bar", "Baz", "" })] + [InlineData("Foo Baz Bar", default(char), new[] { "Foo Baz Bar" })] + [InlineData("Foo Baz \x0000 Bar", default(char), new[] { "Foo Baz ", " Bar" })] + [InlineData("Foo Baz \x0000 Bar\x0000", default(char), new[] { "Foo Baz ", " Bar", "" })] + public static void SpanSplitCharSeparator(string valueParam, char separator, string[] expectedParam) + { + char[][] expected = expectedParam.Select(x => x.ToCharArray()).ToArray(); + AssertEqual(expected, valueParam, valueParam.AsSpan().Split(separator)); + } + + [Theory] + [InlineData("", new[] { "" })] + [InlineData(" ", new[] { "", "" })] + [InlineData(" ", new[] { "", "", "", "", "", "" })] + [InlineData(" ", new[] { "", "", "" })] + [InlineData("ab", new[] { "ab" })] + [InlineData("a b", new[] { "a", "b" })] + [InlineData("a ", new[] { "a", "" })] + [InlineData(" b", new[] { "", "b" })] + [InlineData("Foo Bar Baz", new[] { "Foo", "Bar", "Baz" })] + [InlineData("Foo Bar Baz ", new[] { "Foo", "Bar", "Baz", "" })] + [InlineData(" Foo Bar Baz ", new[] { "", "Foo", "Bar", "Baz", "" })] + [InlineData(" Foo Bar Baz ", new[] { "", "Foo", "", "Bar", "Baz", "" })] + public static void SpanSplitDefaultCharSeparator(string valueParam, string[] expectedParam) + { + char[][] expected = expectedParam.Select(x => x.ToCharArray()).ToArray(); + AssertEqual(expected, valueParam, valueParam.AsSpan().Split()); + } + + [Theory] + [InlineData(" Foo Bar Baz,", ", ", new[] { " Foo Bar Baz," })] + [InlineData(" Foo Bar Baz, ", ", ", new[] { " Foo Bar Baz", "" })] + [InlineData(", Foo Bar Baz, ", ", ", new[] { "", "Foo Bar Baz", "" })] + [InlineData(", Foo, Bar, Baz, ", ", ", new[] { "", "Foo", "Bar", "Baz", "" })] + [InlineData(", , Foo Bar, Baz", ", ", new[] { "", "", "Foo Bar", "Baz" })] + [InlineData(", , Foo Bar, Baz, , ", ", ", new[] { "", "", "Foo Bar", "Baz", "", "" })] + [InlineData(", , , , , ", ", ", new[] { "", "", "", "", "", "" })] + [InlineData(" ", " ", new[] { "", "", "", "", "", "" })] + [InlineData(" Foo, Bar Baz ", " ", new[] { "", "Foo, Bar", "Baz", "" })] + public static void SpanSplitStringSeparator(string valueParam, string separator, string[] expectedParam) + { + char[][] expected = expectedParam.Select(x => x.ToCharArray()).ToArray(); + AssertEqual(expected, valueParam, valueParam.AsSpan().Split(separator)); + } + + private static void AssertEqual(T[][] items, ReadOnlySpan orig, SpanSplitEnumerator source) where T : IEquatable + { + foreach (var item in items) + { + Assert.True(source.MoveNext()); + var slice = orig[source.Current]; + Assert.Equal(item, slice.ToArray()); + } + Assert.False(source.MoveNext()); + } + } +} diff --git a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj index d9b36dc5649f8e..952b6d8f9dff5d 100644 --- a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj +++ b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj @@ -165,6 +165,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index fba466adf70904..2467a8d077ab62 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -399,6 +399,7 @@ + @@ -839,6 +840,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.Split.cs b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.Split.cs new file mode 100644 index 00000000000000..77426e5843e6c9 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.Split.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System +{ + public static partial class MemoryExtensions + { + /// + /// Returns a type that allows for enumeration of each element within a split span + /// using a single space as a separator character. + /// + /// The source span to be enumerated. + /// Returns a . + public static SpanSplitEnumerator Split(this ReadOnlySpan span) + => new SpanSplitEnumerator(span, ' '); + + /// + /// Returns a type that allows for enumeration of each element within a split span + /// using the provided separator character. + /// + /// The source span to be enumerated. + /// The separator character to be used to split the provided span. + /// Returns a . + public static SpanSplitEnumerator Split(this ReadOnlySpan span, char separator) + => new SpanSplitEnumerator(span, separator); + + /// + /// Returns a type that allows for enumeration of each element within a split span + /// using the provided separator string. + /// + /// The source span to be enumerated. + /// The separator string to be used to split the provided span. + /// Returns a . + public static SpanSplitEnumerator Split(this ReadOnlySpan span, string separator) + => new SpanSplitEnumerator(span, separator ?? string.Empty); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/SpanSplitEnumerator.T.cs b/src/libraries/System.Private.CoreLib/src/System/SpanSplitEnumerator.T.cs new file mode 100644 index 00000000000000..69382459b26e4a --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/SpanSplitEnumerator.T.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System +{ + /// + /// allows for enumeration of each element within a + /// that has been split using a provided separator. + /// + public ref struct SpanSplitEnumerator where T : IEquatable + { + private readonly ReadOnlySpan _buffer; + + private readonly ReadOnlySpan _separators; + private readonly T _separator; + + private readonly int _separatorLength; + private readonly bool _splitOnSingleToken; + + private readonly bool _isInitialized; + + private int _startCurrent; + private int _endCurrent; + private int _startNext; + + /// + /// Returns an enumerator that allows for iteration over the split span. + /// + /// Returns a that can be used to iterate over the split span. + public SpanSplitEnumerator GetEnumerator() => this; + + /// + /// Returns the current element of the enumeration. + /// + /// Returns a instance that indicates the bounds of the current element withing the source span. + public Range Current => new Range(_startCurrent, _endCurrent); + + internal SpanSplitEnumerator(ReadOnlySpan span, ReadOnlySpan separators) + { + _isInitialized = true; + _buffer = span; + _separators = separators; + _separator = default!; + _splitOnSingleToken = false; + _separatorLength = _separators.Length != 0 ? _separators.Length : 1; + _startCurrent = 0; + _endCurrent = 0; + _startNext = 0; + } + + internal SpanSplitEnumerator(ReadOnlySpan span, T separator) + { + _isInitialized = true; + _buffer = span; + _separator = separator; + _separators = default; + _splitOnSingleToken = true; + _separatorLength = 1; + _startCurrent = 0; + _endCurrent = 0; + _startNext = 0; + } + + /// + /// Advances the enumerator to the next element of the enumeration. + /// + /// if the enumerator was successfully advanced to the next element; if the enumerator has passed the end of the enumeration. + public bool MoveNext() + { + if (!_isInitialized || _startNext > _buffer.Length) + { + return false; + } + + ReadOnlySpan slice = _buffer.Slice(_startNext); + _startCurrent = _startNext; + + int separatorIndex = _splitOnSingleToken ? slice.IndexOf(_separator) : slice.IndexOf(_separators); + int elementLength = (separatorIndex != -1 ? separatorIndex : slice.Length); + + _endCurrent = _startCurrent + elementLength; + _startNext = _endCurrent + _separatorLength; + return true; + } + } +}