Repurpose Span<T>.Split to accept generic

This commit is contained in:
Oliver Booth 2022-11-29 12:39:34 +00:00
parent ed8651172b
commit 53e8b2ff64
No known key found for this signature in database
GPG Key ID: 32A00B35503AF634
5 changed files with 295 additions and 121 deletions

View File

@ -30,14 +30,16 @@
- X10D: Added `PopCount()` for built-in integer types
- X10D: Added `ReadOnlySpan<char>.CountSubstring(char)`
- X10D: Added `ReadOnlySpan<char>.CountSubstring(ReadOnlySpan<char>[, StringComparison])`
- X10D: Added `ReadOnlySpan<char>.Split(ReadOnlySpan<char>[, StringComparison])`
- X10D: Added `ReadOnlySpan<T>.Split(T)`
- X10D: Added `ReadOnlySpan<T>.Split(ReadOnlySpan<T>)`
- X10D: Added `RoundUpToPowerOf2()` for built-in integer types
- X10D: Added `Size.ToPoint()`
- X10D: Added `Size.ToPointF()`
- X10D: Added `Size.ToVector2()`
- X10D: Added `Span<char>.CountSubstring(char)`
- X10D: Added `Span<char>.CountSubstring(Span<char>[, StringComparison])`
- X10D: Added `Span<char>.Split(char, Span<Range>)`
- X10D: Added `Span<T>.Split(T)`
- X10D: Added `Span<T>.Split(Span<T>)`
- X10D: Added `string.CountSubstring(char)`
- X10D: Added `string.CountSubstring(string[, StringComparison])`
- X10D: Added `Quaternion.Multiply(Vector3)` - this functions as an equivalent to Unity's `Quaternion * Vector3` operator

View File

@ -0,0 +1,91 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using X10D.Collections;
namespace X10D.Tests.Collections;
[TestClass]
public class SpanTest
{
[TestMethod]
public void Split_OnEmptySpan_ShouldYieldNothing()
{
ReadOnlySpan<char> span = ReadOnlySpan<char>.Empty;
var index = 0;
foreach (ReadOnlySpan<char> unused in span.Split(' '))
{
index++;
}
Assert.AreEqual(0, index);
}
[TestMethod]
public void Split_OnOneWord_ShouldYieldLength1()
{
ReadOnlySpan<char> span = "Hello ".AsSpan();
var index = 0;
foreach (ReadOnlySpan<char> subSpan in span.Split(' '))
{
if (index == 0)
{
Assert.AreEqual("Hello", subSpan.ToString());
}
index++;
}
Assert.AreEqual(1, index);
}
[TestMethod]
public void Split_OnTwoWords_ShouldYieldLength2()
{
ReadOnlySpan<char> span = "Hello World ".AsSpan();
var index = 0;
foreach (ReadOnlySpan<char> subSpan in span.Split(' '))
{
if (index == 0)
{
Assert.AreEqual("Hello", subSpan.ToString());
}
else if (index == 1)
{
Assert.AreEqual("World", subSpan.ToString());
}
index++;
}
Assert.AreEqual(2, index);
}
[TestMethod]
public void Split_OnThreeWords_ShouldYieldLength3()
{
ReadOnlySpan<char> span = "Hello, the World ".AsSpan();
var index = 0;
foreach (ReadOnlySpan<char> subSpan in span.Split(' '))
{
if (index == 0)
{
Assert.AreEqual("Hello,", subSpan.ToString());
}
else if (index == 1)
{
Assert.AreEqual("the", subSpan.ToString());
}
else if (index == 2)
{
Assert.AreEqual("World", subSpan.ToString());
}
index++;
}
Assert.AreEqual(3, index);
}
}

View File

@ -36,53 +36,4 @@ public class CharSpanTests
Assert.AreEqual(0, string.Empty.AsSpan().CountSubstring('\0'));
Assert.AreEqual(0, string.Empty.AsSpan().CountSubstring(string.Empty.AsSpan(), StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
public void Split_OnEmptySpan_ShouldYieldNothing()
{
ReadOnlySpan<char> span = ReadOnlySpan<char>.Empty;
Assert.AreEqual(0, span.Split(' ', Span<Range>.Empty));
}
[TestMethod]
public void Split_OnOneWord_ShouldYieldLength1()
{
ReadOnlySpan<char> span = "Hello".AsSpan();
Span<Range> wordRanges = stackalloc Range[1];
Assert.AreEqual(1, span.Split(' ', wordRanges));
Assert.AreEqual(..5, wordRanges[0]);
Assert.AreEqual("Hello", span[wordRanges[0]].ToString());
}
[TestMethod]
public void Split_OnTwoWords_ShouldYieldLength2()
{
ReadOnlySpan<char> span = "Hello World".AsSpan();
Span<Range> wordRanges = stackalloc Range[2];
Assert.AreEqual(2, span.Split(' ', wordRanges));
Assert.AreEqual(..5, wordRanges[0]);
Assert.AreEqual(6..11, wordRanges[1]);
Assert.AreEqual("Hello", span[wordRanges[0]].ToString());
Assert.AreEqual("World", span[wordRanges[1]].ToString());
}
[TestMethod]
public void Split_OnThreeWords_ShouldYieldLength2()
{
ReadOnlySpan<char> span = "Hello, the World".AsSpan();
Span<Range> wordRanges = stackalloc Range[3];
Assert.AreEqual(3, span.Split(' ', wordRanges));
Assert.AreEqual(..6, wordRanges[0]);
Assert.AreEqual(7..10, wordRanges[1]);
Assert.AreEqual(11..16, wordRanges[2]);
Assert.AreEqual("Hello,", span[wordRanges[0]].ToString());
Assert.AreEqual("the", span[wordRanges[1]].ToString());
Assert.AreEqual("World", span[wordRanges[2]].ToString());
}
}

View File

@ -0,0 +1,200 @@
namespace X10D.Collections;
/// <summary>
/// Extension methods for <see cref="Span{T}" /> and <see cref="ReadOnlySpan{T}" />
/// </summary>
public static class SpanExtensions
{
/// <summary>
/// Returns the number of times that a specified element appears in a span of elements of the same type.
/// </summary>
/// <param name="source">The source to search.</param>
/// <param name="element">The element to count.</param>
/// <typeparam name="T">The type of elements in <paramref name="source" />.</typeparam>
/// <returns>The number of times that <paramref name="element" /> appears in <paramref name="source" />.</returns>
public static int Count<T>(this in Span<T> source, T element)
where T : IEquatable<T>
{
return source.AsReadOnly().Count(element);
}
/// <summary>
/// Returns the number of times that a specified element appears in a read-only span of elements of the same type.
/// </summary>
/// <param name="source">The source to search.</param>
/// <param name="element">The element to count.</param>
/// <typeparam name="T">The type of elements in <paramref name="source" />.</typeparam>
/// <returns>The number of times that <paramref name="element" /> appears in <paramref name="source" />.</returns>
public static int Count<T>(this in ReadOnlySpan<T> source, T element)
where T : IEquatable<T>
{
var count = 0;
foreach (T item in source)
{
if (item.Equals(element))
{
count++;
}
}
return count;
}
/// <summary>
/// Returns a read-only <see cref="ReadOnlySpan{T}" /> wrapper for the current span.
/// </summary>
/// <param name="source">The source span.</param>
/// <typeparam name="T">The type of elements in <paramref name="source" />.</typeparam>
/// <returns>A <see cref="ReadOnlySpan{T}" /> which wraps the elements in <paramref name="source" />.</returns>
public static ReadOnlySpan<T> AsReadOnly<T>(this in Span<T> source)
{
return source;
}
/// <summary>
/// Splits a span of elements into sub-spans based on a delimiting element.
/// </summary>
/// <param name="source">The span to split.</param>
/// <param name="delimiter">The delimiting element.</param>
/// <typeparam name="T">The type of elements in <paramref name="source" />.</typeparam>
/// <returns>
/// An enumerator which wraps <paramref name="source"/> and delimits the elements based on <paramref name="delimiter" />.
/// </returns>
public static SpanSplitEnumerator<T> Split<T>(this in Span<T> source, T delimiter)
where T : struct, IEquatable<T>
{
return source.AsReadOnly().Split(delimiter);
}
/// <summary>
/// Splits a span of elements into sub-spans based on a delimiting element.
/// </summary>
/// <param name="source">The span to split.</param>
/// <param name="delimiter">The delimiting element.</param>
/// <typeparam name="T">The type of elements in <paramref name="source" />.</typeparam>
/// <returns>
/// An enumerator which wraps <paramref name="source"/> and delimits the elements based on <paramref name="delimiter" />.
/// </returns>
public static SpanSplitEnumerator<T> Split<T>(this in ReadOnlySpan<T> source, T delimiter)
where T : struct, IEquatable<T>
{
return new SpanSplitEnumerator<T>(source, delimiter);
}
/// <summary>
/// Splits a span of elements into sub-spans based on a span of delimiting elements.
/// </summary>
/// <param name="source">The span to split.</param>
/// <param name="delimiter">The span of delimiting elements.</param>
/// <typeparam name="T">The type of elements in <paramref name="source" />.</typeparam>
/// <returns>
/// An enumerator which wraps <paramref name="source"/> and delimits the elements based on <paramref name="delimiter" />.
/// </returns>
public static SpanSplitEnumerator<T> Split<T>(this in Span<T> source, in ReadOnlySpan<T> delimiter)
where T : struct, IEquatable<T>
{
return source.AsReadOnly().Split(delimiter);
}
/// <summary>
/// Splits a span of elements into sub-spans based on a span of delimiting elements.
/// </summary>
/// <param name="source">The span to split.</param>
/// <param name="delimiter">The span of delimiting elements.</param>
/// <typeparam name="T">The type of elements in <paramref name="source" />.</typeparam>
/// <returns>
/// An enumerator which wraps <paramref name="source"/> and delimits the elements based on <paramref name="delimiter" />.
/// </returns>
public static SpanSplitEnumerator<T> Split<T>(this in ReadOnlySpan<T> source, in ReadOnlySpan<T> delimiter)
where T : struct, IEquatable<T>
{
return new SpanSplitEnumerator<T>(source, delimiter);
}
/// <summary>
/// Enumerates the elements of a <see cref="ReadOnlySpan{T}" />.
/// </summary>
/// <typeparam name="T">The type of elements in the span.</typeparam>
public ref struct SpanSplitEnumerator<T> where T : struct, IEquatable<T>
{
private ReadOnlySpan<T> _source;
private readonly ReadOnlySpan<T> _delimiterSpan;
private readonly T _delimiter;
private readonly bool _usingSpanDelimiter;
/// <summary>
/// Initializes a new instance of the <see cref="SpanSplitEnumerator{T}" /> struct.
/// </summary>
/// <param name="source">The source span.</param>
/// <param name="delimiter">The delimiting span of elements.</param>
public SpanSplitEnumerator(in ReadOnlySpan<T> source, ReadOnlySpan<T> delimiter)
{
_usingSpanDelimiter = true;
_source = source;
_delimiter = default;
_delimiterSpan = delimiter;
Current = ReadOnlySpan<T>.Empty;
}
/// <summary>
/// Initializes a new instance of the <see cref="SpanSplitEnumerator{T}" /> struct.
/// </summary>
/// <param name="source">The source span.</param>
/// <param name="delimiter">The delimiting element.</param>
public SpanSplitEnumerator(in ReadOnlySpan<T> source, T delimiter)
{
_usingSpanDelimiter = false;
_source = source;
_delimiter = delimiter;
_delimiterSpan = ReadOnlySpan<T>.Empty;
Current = ReadOnlySpan<T>.Empty;
}
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
/// <value>The element in the <see cref="ReadOnlySpan{T}" /> at the current position of the enumerator.</value>
public ReadOnlySpan<T> Current { get; private set; }
/// <summary>
/// Returns the current enumerator.
/// </summary>
/// <returns>The current instance of <see cref="SpanSplitEnumerator{T}" />.</returns>
/// <remarks>
/// This method exists to provide the ability to enumerate within a <c>foreach</c> loop. It should not be called
/// manually.
/// </remarks>
public readonly SpanSplitEnumerator<T> GetEnumerator()
{
return this;
}
/// <summary>
/// Advances the enumerator to the next element of the <see cref="ReadOnlySpan{T}" />.
/// </summary>
/// <returns>
/// <see langword="true" /> if the enumerator was successfully advanced to the next element; <see langword="false" />
/// if the enumerator has passed the end of the span.
/// </returns>
public bool MoveNext()
{
if (_source.Length == 0)
{
return false;
}
int index = _usingSpanDelimiter ? _source.IndexOf(_delimiterSpan) : _source.IndexOf(_delimiter);
if (index == -1)
{
Current = _source;
_source = ReadOnlySpan<T>.Empty;
return true;
}
Current = _source[..index];
_source = _source[(index + 1)..];
return true;
}
}
}

View File

@ -67,74 +67,4 @@ public static class CharSpanExtensions
return count;
}
/// <summary>
/// Splits a span of characters into substrings based on a specific delimiting character.
/// </summary>
/// <param name="value">The span of characters to split.</param>
/// <param name="separator">A character that delimits the substring in this character span.</param>
/// <param name="destination">
/// When this method returns, will be populated with the <see cref="Range" /> values pointing to where each substring
/// starts and ends in <paramref name="value" />.
/// </param>
/// <returns>
/// The number of substrings within <paramref name="value" />. This value is always correct regardless of the length of
/// <paramref name="destination" />.
/// </returns>
public static int Split(this Span<char> value, char separator, Span<Range> destination)
{
return ((ReadOnlySpan<char>)value).Split(separator, destination);
}
/// <summary>
/// Splits a span of characters into substrings based on a specific delimiting character.
/// </summary>
/// <param name="value">The span of characters to split.</param>
/// <param name="separator">A character that delimits the substring in this character span.</param>
/// <param name="destination">
/// When this method returns, will be populated with the <see cref="Range" /> values pointing to where each substring
/// starts and ends in <paramref name="value" />.
/// </param>
/// <returns>
/// The number of substrings within <paramref name="value" />. This value is always correct regardless of the length of
/// <paramref name="destination" />.
/// </returns>
public static int Split(this ReadOnlySpan<char> value, char separator, Span<Range> destination)
{
Span<char> buffer = stackalloc char[value.Length];
var matches = 0;
for (int index = 0, bufferLength = 0, startIndex = 0; index < value.Length; index++)
{
bool end = index == value.Length - 1;
if (end)
{
bufferLength++;
}
if (value[index] == separator || end)
{
if (destination.Length > matches)
{
// I was going to use new Range(startIndex, startIndex + bufferLength)
// but the .. operator is just so fucking cool so +1 for brevity over
// clarity!
// ... Ok I know this is probably a bad idea but come on, isn't it neat
// that you can use any integer expression as either operand to the .. operator?
// SOMEBODY AGREE WITH ME!
destination[matches] = startIndex..(startIndex + bufferLength);
}
startIndex = index + 1;
bufferLength = 0;
matches++;
}
else
{
buffer[bufferLength++] = value[index];
}
}
return matches;
}
}