Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/libraries/System.Memory/ref/System.Memory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,10 @@ public static void Sort<TKey, TValue>(this System.Span<TKey> keys, System.Span<T
public static void Sort<TKey, TValue>(this System.Span<TKey> keys, System.Span<TValue> items, System.Comparison<TKey> comparison) { }
public static void Sort<T, TComparer>(this System.Span<T> span, TComparer comparer) where TComparer : System.Collections.Generic.IComparer<T>? { }
public static void Sort<TKey, TValue, TComparer>(this System.Span<TKey> keys, System.Span<TValue> items, TComparer comparer) where TComparer : System.Collections.Generic.IComparer<TKey>? { }
public static System.MemoryExtensions.SpanSplitEnumerator<T> Split<T>(this System.ReadOnlySpan<T> source, T separator) where T : IEquatable<T> { throw null; }
public static System.MemoryExtensions.SpanSplitEnumerator<T> Split<T>(this System.ReadOnlySpan<T> source, System.ReadOnlySpan<T> separator) where T : IEquatable<T> { throw null; }
public static System.MemoryExtensions.SpanSplitEnumerator<T> SplitAny<T>(this System.ReadOnlySpan<T> source, [System.Diagnostics.CodeAnalysis.UnscopedRef] params System.ReadOnlySpan<T> separators) where T : IEquatable<T> { throw null; }
public static System.MemoryExtensions.SpanSplitEnumerator<T> SplitAny<T>(this System.ReadOnlySpan<T> source, System.Buffers.SearchValues<T> separators) where T : IEquatable<T> { throw null; }
public static int Split(this System.ReadOnlySpan<char> source, System.Span<System.Range> destination, char separator, System.StringSplitOptions options = System.StringSplitOptions.None) { throw null; }
public static int Split(this System.ReadOnlySpan<char> source, System.Span<System.Range> destination, System.ReadOnlySpan<char> separator, System.StringSplitOptions options = System.StringSplitOptions.None) { throw null; }
public static int SplitAny(this System.ReadOnlySpan<char> source, System.Span<System.Range> destination, System.ReadOnlySpan<char> separators, System.StringSplitOptions options = System.StringSplitOptions.None) { throw null; }
Expand Down Expand Up @@ -430,6 +434,12 @@ public ref struct TryWriteInterpolatedStringHandler
public bool AppendFormatted(string? value) { throw null; }
public bool AppendFormatted(string? value, int alignment = 0, string? format = null) { throw null; }
}
public ref struct SpanSplitEnumerator<T> where T : System.IEquatable<T>
{
public System.MemoryExtensions.SpanSplitEnumerator<T> GetEnumerator() { throw null; }
public readonly System.Range Current { get { throw null; } }
public bool MoveNext() { throw null; }
}
}
}
namespace System.Buffers
Expand Down
229 changes: 229 additions & 0 deletions src/libraries/System.Memory/tests/ReadOnlySpan/Split.T.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// 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.Buffers;
using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace System.SpanTests
{
public static partial class ReadOnlySpanTests
{
public record struct CustomStruct(int value) : IEquatable<CustomStruct>;
public record class CustomClass(int value) : IEquatable<CustomClass>;

[Fact]
public static void DefaultSpanSplitEnumeratorBehaviour()
{
var charSpanEnumerator = new MemoryExtensions.SpanSplitEnumerator<char>();
Assert.Equal(new Range(0, 0), charSpanEnumerator.Current);
Assert.False(charSpanEnumerator.MoveNext());

// Implicit DoesNotThrow assertion
charSpanEnumerator.GetEnumerator();

var stringSpanEnumerator = new MemoryExtensions.SpanSplitEnumerator<char>();
Assert.Equal(new Range(0, 0), stringSpanEnumerator.Current);
Assert.False(stringSpanEnumerator.MoveNext());
stringSpanEnumerator.GetEnumerator();
}

public static IEnumerable<object[]> SplitSingleElementSeparatorData =>
[
// Split on default
[ (char[])['a', ' ', 'b'], default(char), (Range[])[0..3] ],
[ (int[]) [1, 2, 3], default(int), (Range[])[0..3] ],
[ (long[])[1, 2, 3], default(long), (Range[])[0..3] ],
[ (byte[])[1, 2, 3], default(byte), (Range[])[0..3] ],
[ (CustomStruct[])[new(1), new(2), new(3)], default(CustomStruct), (Range[])[0..3] ],
[ (CustomClass[])[new(1), new(2), new(3)], default(CustomClass), (Range[])[0..3] ],

// Split no matching element
[ (char[])['a', ' ', 'b'], ',', (Range[])[0..3] ],
[ (int[]) [1, 2, 3], (int)4, (Range[])[0..3] ],
[ (long[])[1, 2, 3], (long)4, (Range[])[0..3] ],
[ (byte[])[1, 2, 3], (byte)4, (Range[])[0..3] ],
[ (CustomStruct[])[new(1), new(2), new(3)], new CustomStruct(4), (Range[])[0..3] ],
[ (CustomClass[])[new(1), new(2), new(3)], new CustomClass(4), (Range[])[0..3] ],

// Split on sequence containing only a separator
[ (char[])[','], ',', (Range[])[0..0, 1..1] ],
[ (int[]) [1], (int)1, (Range[])[0..0, 1..1] ],
[ (long[])[1], (long)1, (Range[])[0..0, 1..1] ],
[ (byte[])[1], (byte)1, (Range[])[0..0, 1..1] ],
[ (CustomStruct[])[new(1)], new CustomStruct(1), (Range[])[0..0, 1..1] ],
[ (CustomClass[]) [new(1)], new CustomClass(1), (Range[])[0..0, 1..1] ],

// Split on empty sequence with default separator
[ (char[])[], default(char), (Range[])[0..0] ],
[ (int[]) [], default(int), (Range[])[0..0] ],
[ (long[])[], default(long), (Range[])[0..0] ],
[ (byte[])[], default(byte), (Range[])[0..0] ],
[ (CustomStruct[])[], default(CustomStruct), (Range[])[0..0] ],
[ (CustomClass[]) [], default(CustomClass), (Range[])[0..0] ],

[ (char[])['a', ',', 'b'], ',', (Range[]) [ 0..1, 2..3 ] ],
[ (int[]) [1, 2, 3], (int)2, (Range[]) [ 0..1, 2..3 ] ],
[ (long[])[1, 2, 3], (long)2, (Range[]) [ 0..1, 2..3 ] ],
[ (byte[])[1, 2, 3], (byte)2, (Range[]) [ 0..1, 2..3 ] ],
[ (CustomStruct[])[new(1), new(2), new(3)], new CustomStruct(2), (Range[]) [ 0..1, 2..3 ] ],
[ (CustomClass[])[new(1), new(2), new(3)], new CustomClass(2), (Range[]) [ 0..1, 2..3 ] ],

[ (char[])['a', 'b', ',', ','], ',', (Range[]) [ 0..2, 3..3, 4..4 ] ],
[ (int[]) [1, 3, 2, 2], (int)2, (Range[]) [ 0..2, 3..3, 4..4 ] ],
[ (long[])[1, 3, 2, 2], (long)2, (Range[]) [ 0..2, 3..3, 4..4 ] ],
[ (byte[])[1, 3, 2, 2], (byte)2, (Range[]) [ 0..2, 3..3, 4..4 ] ],
[ (CustomStruct[])[new(1), new(3), new(2), new(2)], new CustomStruct(2), (Range[]) [ 0..2, 3..3, 4..4 ] ],
[ (CustomClass[])[new(1), new(3), new(2), new(2)], new CustomClass(2), (Range[]) [ 0..2, 3..3, 4..4 ] ],
];

[Theory]
[MemberData(nameof(SplitSingleElementSeparatorData))]
public static void Split_SingleElementSeparator<T>(T[] value, T separator, Range[] result) where T : IEquatable<T>
{
AssertEnsureCorrectEnumeration(new ReadOnlySpan<T>(value).Split(separator), result);
}

public static IEnumerable<object[]> SplitSequenceSeparatorData =>
[
// Split no separators
[ (char[])['a', ' ', 'b'], (char[])[], (Range[])[0..3] ],
[ (int[]) [1, 2, 3], (int[]) [], (Range[])[0..3] ],
[ (long[])[1, 2, 3], (long[])[], (Range[])[0..3] ],
[ (byte[])[1, 2, 3], (byte[])[], (Range[])[0..3] ],
[ (CustomStruct[])[new(1), new(2), new(3)], (CustomStruct[])[], (Range[])[0..3] ],
[ (CustomClass[])[new(1), new(2), new(3)], (CustomClass[])[], (Range[])[0..3] ],

// Split no matching elements
[ (char[])['a', ' ', 'b'], (char[])[',', '.' ], (Range[])[0..3] ],
[ (int[]) [1, 2, 3], (int[]) [4, 3], (Range[])[0..3] ],
[ (long[])[1, 2, 3], (long[])[4, 3], (Range[])[0..3] ],
[ (byte[])[1, 2, 3], (byte[])[4, 3], (Range[])[0..3] ],
[ (CustomStruct[])[new(1), new(2), new(3)], (CustomStruct[])[new(4), new(3)], (Range[])[0..3] ],
[ (CustomClass[])[new(1), new(2), new(3)], (CustomClass[])[new(4), new(3)], (Range[])[0..3] ],

// Split on input span with only a single sequence separator
[ (char[])[',', '.'], (char[])[',', '.' ], (Range[])[0..0, 2..2] ],
[ (int[]) [4, 3], (int[]) [4, 3], (Range[])[0..0, 2..2] ],
[ (long[])[4, 3], (long[])[4, 3], (Range[])[0..0, 2..2] ],
[ (byte[])[4, 3], (byte[])[4, 3], (Range[])[0..0, 2..2] ],
[ (CustomStruct[])[new(4), new(3)], (CustomStruct[])[new(4), new(3)], (Range[])[0..0, 2..2] ],
[ (CustomClass[])[new(4), new(3)], (CustomClass[])[new(4), new(3)], (Range[])[0..0, 2..2] ],

// Split on empty sequence with default separator
[ (char[])[], (char[])[default(char)], (Range[])[0..0] ],
[ (int[]) [], (int[]) [default(int)], (Range[])[0..0] ],
[ (long[])[], (long[])[default(long)], (Range[])[0..0] ],
[ (byte[])[], (byte[])[default(byte)], (Range[])[0..0] ],
[ (CustomStruct[])[], (CustomStruct[])[default], (Range[])[0..0] ],
[ (CustomClass[]) [], (CustomClass[])[default], (Range[])[0..0] ],

[ (char[])['a', ',', '-', 'b'], (char[])[',', '-'], (Range[]) [ 0..1, 3..4 ] ],
[ (int[]) [1, 2, 4, 3], (int[])[2, 4], (Range[]) [ 0..1, 3..4 ] ],
[ (long[])[1, 2, 4, 3], (long[])[2, 4], (Range[]) [ 0..1, 3..4 ] ],
[ (byte[])[1, 2, 4, 3], (byte[])[2, 4], (Range[]) [ 0..1, 3..4 ] ],
[ (CustomStruct[])[new(1), new(2), new(4), new(3)], (CustomStruct[]) [new(2), new(4)], (Range[]) [ 0..1, 3..4 ] ],
[ (CustomClass[])[new(1), new(2), new(4), new(3)], (CustomClass[])[new(2), new(4)], (Range[]) [ 0..1, 3..4 ] ],

[ (char[])[',', '-', 'a', ',', '-', 'b'], (char[])[',', '-'], (Range[]) [ 0..0, 2..3, 5..6 ] ],
[ (int[]) [2, 4, 3, 2, 4, 5], (int[]) [2, 4], (Range[]) [ 0..0, 2..3, 5..6 ] ],
[ (long[])[2, 4, 3, 2, 4, 5], (long[])[2, 4], (Range[]) [ 0..0, 2..3, 5..6 ] ],
[ (byte[])[2, 4, 3, 2, 4, 5], (byte[])[2, 4], (Range[]) [ 0..0, 2..3, 5..6 ] ],
[ (CustomStruct[])[new(2), new(4), new(3), new(2), new(4), new(5)], (CustomStruct[]) [new(2), new(4)], (Range[]) [ 0..0, 2..3, 5..6 ] ],
[ (CustomClass[])[new(2), new(4), new(3), new(2), new(4), new(5)], (CustomClass[])[new(2), new(4)], (Range[]) [ 0..0, 2..3, 5..6 ] ],
];

[Theory]
[MemberData(nameof(SplitSequenceSeparatorData))]
public static void Split_SequenceSeparator<T>(T[] value, T[] separator, Range[] result) where T : IEquatable<T>
{
AssertEnsureCorrectEnumeration(new ReadOnlySpan<T>(value).Split(separator), result);
}

public static IEnumerable<object[]> SplitAnySeparatorData =>
[
// Split no separators
[ (char[])['a', ' ', 'b'], (char[])[], (Range[])[0..1, 2..3] ], // an empty span of separators for char is handled as all whitespace being separators
[ (int[]) [1, 2, 3], (int[]) [], (Range[])[0..3] ],
[ (long[])[1, 2, 3], (long[])[], (Range[])[0..3] ],
[ (byte[])[1, 2, 3], (byte[])[], (Range[])[0..3] ],
[ (CustomStruct[])[new(1), new(2), new(3)], (CustomStruct[])[], (Range[])[0..3] ],
[ (CustomClass[])[new(1), new(2), new(3)], (CustomClass[])[], (Range[])[0..3] ],

// Split non-matching separators
[ (char[])['a', ' ', 'b'], (char[])[',', '.' ], (Range[])[0..3] ],
[ (int[]) [1, 2, 3], (int[]) [4, 5], (Range[])[0..3] ],
[ (long[])[1, 2, 3], (long[])[4, 5], (Range[])[0..3] ],
[ (byte[])[1, 2, 3], (byte[])[4, 5], (Range[])[0..3] ],
[ (CustomStruct[])[new(1), new(2), new(3)], (CustomStruct[])[new(4), new(5)], (Range[])[0..3] ],
[ (CustomClass[])[new(1), new(2), new(3)], (CustomClass[])[new(4), new(5)], (Range[])[0..3] ],

// Split on sequence containing only a separator
[ (char[])[','], (char[])[','], (Range[])[0..0, 1..1] ],
[ (int[]) [1], (int[]) [1], (Range[])[0..0, 1..1] ],
[ (long[])[1], (long[])[1], (Range[])[0..0, 1..1] ],
[ (byte[])[1], (byte[])[1], (Range[])[0..0, 1..1] ],
[ (CustomStruct[])[new(1)], (CustomStruct[])[new(1)], (Range[])[0..0, 1..1] ],
[ (CustomClass[]) [new(1)], (CustomClass[])[new(1)], (Range[])[0..0, 1..1] ],

// Split on empty sequence with default separator
[ (char[])[], (char[])[default(char)], (Range[])[0..0] ],
[ (int[]) [], (int[]) [default(int)], (Range[])[0..0] ],
[ (long[])[], (long[])[default(long)], (Range[])[0..0] ],
[ (byte[])[], (byte[])[default(byte)], (Range[])[0..0] ],
[ (CustomStruct[])[], (CustomStruct[])[new(default)], (Range[])[0..0] ],
[ (CustomClass[]) [], (CustomClass[])[new(default)], (Range[])[0..0] ],

[ (char[])['a', ',', '-', 'b'], (char[])[',', '-'], (Range[]) [ 0..1, 2..2, 3..4 ] ],
[ (int[]) [1, 2, 4, 3], (int[])[2, 4], (Range[]) [ 0..1, 2..2, 3..4 ] ],
[ (long[])[1, 2, 4, 3], (long[])[2, 4], (Range[]) [ 0..1, 2..2, 3..4 ] ],
[ (byte[])[1, 2, 4, 3], (byte[])[2, 4], (Range[]) [ 0..1, 2..2, 3..4 ] ],
[ (CustomStruct[])[new(1), new(2), new(4), new(3)], (CustomStruct[]) [new(2), new(4)], (Range[]) [ 0..1, 2..2, 3..4 ] ],
[ (CustomClass[])[new(1), new(2), new(4), new(3)], (CustomClass[])[new(2), new(4)], (Range[]) [ 0..1, 2..2, 3..4 ] ],

[ (char[])[',', '-', 'a', ',', '-', 'b'], (char[])[',', '-'], (Range[]) [ 0..0, 1..1, 2..3, 4..4, 5..6 ] ],
[ (int[]) [2, 4, 3, 2, 4, 5], (int[]) [2, 4], (Range[]) [ 0..0, 1..1, 2..3, 4..4, 5..6 ] ],
[ (long[])[2, 4, 3, 2, 4, 5], (long[])[2, 4], (Range[]) [ 0..0, 1..1, 2..3, 4..4, 5..6 ] ],
[ (byte[])[2, 4, 3, 2, 4, 5], (byte[])[2, 4], (Range[]) [ 0..0, 1..1, 2..3, 4..4, 5..6 ] ],
[ (CustomStruct[])[new(2), new(4), new(3), new(2), new(4), new(5)], (CustomStruct[]) [new(2), new(4)], (Range[]) [ 0..0, 1..1, 2..3, 4..4, 5..6 ] ],
[ (CustomClass[])[new(2), new(4), new(3), new(2), new(4), new(5)], (CustomClass[])[new(2), new(4)], (Range[]) [ 0..0, 1..1, 2..3, 4..4, 5..6 ] ],
];

[Theory]
[MemberData(nameof(SplitAnySeparatorData))]
public static void Split_AnySingleElementSeparator<T>(T[] value, T[] separator, Range[] result) where T : IEquatable<T>
{
AssertEnsureCorrectEnumeration(new ReadOnlySpan<T>(value).SplitAny(separator), result);

if (value is char[] source &&
separator is char[] separators &&
separators.Length > 0) // the SearchValues overload does not special-case empty
{
var charEnumerator = new ReadOnlySpan<char>(source).SplitAny(SearchValues.Create(separators));
AssertEnsureCorrectEnumeration(charEnumerator, result);
}
}

private static void AssertEnsureCorrectEnumeration<T>(MemoryExtensions.SpanSplitEnumerator<T> enumerator, Range[] result) where T : IEquatable<T>
{
// Assert.Throws would not work due to the requirement to capture the ref struct
try
{
_ = enumerator.Current;
Assert.Fail("enumerator.Current is not valid until the first call to MoveNext()");
}
catch (ArgumentOutOfRangeException) { }
Assert.True(enumerator.MoveNext());

foreach ((Range r, int index) in result.Select((e, i) => (e, i)))
{
Assert.Equal(r, enumerator.Current);
if (index < result.Length - 1)
Assert.True(enumerator.MoveNext());
}
Assert.False(enumerator.MoveNext());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@
<Compile Include="ReadOnlySpan\SequenceEqual.long.cs" />
<Compile Include="ReadOnlySpan\SequenceEqual.T.cs" />
<Compile Include="ReadOnlySpan\Slice.cs" />
<Compile Include="ReadOnlySpan\Split.T.cs" />
<Compile Include="ReadOnlySpan\StartsWith.byte.cs" />
<Compile Include="ReadOnlySpan\StartsWith.long.cs" />
<Compile Include="ReadOnlySpan\StartsWith.T.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\MemoryExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\MemoryExtensions.Globalization.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\MemoryExtensions.Globalization.Utf8.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\MemoryExtensions.Split.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\MemoryExtensions.Trim.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\MemoryExtensions.Trim.Utf8.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\MethodAccessException.cs" />
Expand Down
Loading