Add [ReadOnly]Span<char>.Split

This commit also migrates CountSubstring from StringExtensions to CharSpanExtensions
This commit is contained in:
Oliver Booth 2022-11-28 02:59:46 +00:00
parent 4af5a712f4
commit ed8651172b
No known key found for this signature in database
GPG Key ID: 32A00B35503AF634
5 changed files with 230 additions and 71 deletions

View File

@ -30,12 +30,14 @@
- 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 `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 `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,88 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using X10D.Text;
namespace X10D.Tests.Text;
[TestClass]
public class CharSpanTests
{
[TestMethod]
public void CountSubstring_ShouldHonor_StringComparison()
{
Assert.AreEqual(0, "Hello World".AsSpan().CountSubstring('E'));
Assert.AreEqual(0, "Hello World".AsSpan().CountSubstring("E".AsSpan()));
Assert.AreEqual(1, "Hello World".AsSpan().CountSubstring("E".AsSpan(), StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
public void CountSubstring_ShouldReturn0_GivenNoInstanceChar()
{
Assert.AreEqual(0, "Hello World".AsSpan().CountSubstring('z'));
Assert.AreEqual(0, "Hello World".AsSpan().CountSubstring("z".AsSpan()));
Assert.AreEqual(0, "Hello World".AsSpan().CountSubstring("z".AsSpan(), StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
public void CountSubstring_ShouldReturn1_GivenSingleInstanceChar()
{
Assert.AreEqual(1, "Hello World".AsSpan().CountSubstring('e'));
Assert.AreEqual(1, "Hello World".AsSpan().CountSubstring("e".AsSpan()));
Assert.AreEqual(1, "Hello World".AsSpan().CountSubstring("e".AsSpan(), StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
public void CountSubstring_ShouldReturn0_GivenEmptyHaystack()
{
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

@ -107,8 +107,6 @@ public class StringTests
Assert.AreEqual(0, "Hello World".CountSubstring('E'));
Assert.AreEqual(0, "Hello World".CountSubstring("E"));
Assert.AreEqual(1, "Hello World".CountSubstring("E", StringComparison.OrdinalIgnoreCase));
Assert.AreEqual(0, "Hello World".AsSpan().CountSubstring('E'));
Assert.AreEqual(1, "Hello World".AsSpan().CountSubstring("E".AsSpan(), StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
@ -117,8 +115,6 @@ public class StringTests
Assert.AreEqual(0, "Hello World".CountSubstring('z'));
Assert.AreEqual(0, "Hello World".CountSubstring("z"));
Assert.AreEqual(0, "Hello World".CountSubstring("z", StringComparison.OrdinalIgnoreCase));
Assert.AreEqual(0, "Hello World".AsSpan().CountSubstring('z'));
Assert.AreEqual(0, "Hello World".AsSpan().CountSubstring("z".AsSpan(), StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
@ -127,8 +123,6 @@ public class StringTests
Assert.AreEqual(1, "Hello World".CountSubstring('e'));
Assert.AreEqual(1, "Hello World".CountSubstring("e"));
Assert.AreEqual(1, "Hello World".CountSubstring("e", StringComparison.OrdinalIgnoreCase));
Assert.AreEqual(1, "Hello World".AsSpan().CountSubstring('e'));
Assert.AreEqual(1, "Hello World".AsSpan().CountSubstring("e".AsSpan(), StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
@ -137,8 +131,6 @@ public class StringTests
Assert.AreEqual(0, string.Empty.CountSubstring('\0'));
Assert.AreEqual(0, string.Empty.CountSubstring(string.Empty));
Assert.AreEqual(0, string.Empty.CountSubstring(string.Empty, StringComparison.OrdinalIgnoreCase));
Assert.AreEqual(0, string.Empty.AsSpan().CountSubstring('\0'));
Assert.AreEqual(0, string.Empty.AsSpan().CountSubstring(string.Empty.AsSpan(), StringComparison.OrdinalIgnoreCase));
}
[TestMethod]

View File

@ -0,0 +1,140 @@
namespace X10D.Text;
/// <summary>
/// Extension methods for <see cref="ReadOnlySpan{T}" /> and <see cref="Span{T}" /> of <see cref="char" />.
/// </summary>
public static class CharSpanExtensions
{
/// <summary>
/// Counts the occurrences of a substring within the current character span.
/// </summary>
/// <param name="haystack">The haystack search space.</param>
/// <param name="needle">The character span to count.</param>
/// <returns>An integer representing the count of <paramref name="needle" /> inside <paramref name="haystack" />.</returns>
public static int CountSubstring(this Span<char> haystack, Span<char> needle)
{
return CountSubstring(haystack, needle, StringComparison.Ordinal);
}
/// <summary>
/// Counts the occurrences of a substring within the current character span, using a specified string comparison method.
/// </summary>
/// <param name="haystack">The haystack search space.</param>
/// <param name="needle">The character span to count.</param>
/// <param name="comparison">The string comparison method used for determining substring count.</param>
/// <returns>An integer representing the count of <paramref name="needle" /> inside <paramref name="haystack" />.</returns>
public static int CountSubstring(this Span<char> haystack, Span<char> needle, StringComparison comparison)
{
return CountSubstring((ReadOnlySpan<char>)haystack, needle, comparison);
}
/// <summary>
/// Counts the occurrences of a substring within the current character span.
/// </summary>
/// <param name="haystack">The haystack search space.</param>
/// <param name="needle">The character span to count.</param>
/// <returns>An integer representing the count of <paramref name="needle" /> inside <paramref name="haystack" />.</returns>
public static int CountSubstring(this ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle)
{
return CountSubstring(haystack, needle, StringComparison.Ordinal);
}
/// <summary>
/// Counts the occurrences of a substring within the current character span, using a specified string comparison method.
/// </summary>
/// <param name="haystack">The haystack search space.</param>
/// <param name="needle">The character span to count.</param>
/// <param name="comparison">The string comparison method used for determining substring count.</param>
/// <returns>An integer representing the count of <paramref name="needle" /> inside <paramref name="haystack" />.</returns>
public static int CountSubstring(this ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle, StringComparison comparison)
{
if (haystack.IsEmpty || needle.IsEmpty)
{
return 0;
}
int haystackLength = haystack.Length;
int needleLength = needle.Length;
var count = 0;
for (var index = 0; index < haystackLength - needleLength - 1; index++)
{
if (haystack[index..(index + needleLength)].Equals(needle, comparison))
{
count++;
}
}
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;
}
}

View File

@ -214,69 +214,6 @@ public static class StringExtensions
return haystack.AsSpan().CountSubstring(needle);
}
/// <summary>
/// Counts the occurrences of a substring within the current character span.
/// </summary>
/// <param name="haystack">The haystack search space.</param>
/// <param name="needle">The character span to count.</param>
/// <returns>An integer representing the count of <paramref name="needle" /> inside <paramref name="haystack" />.</returns>
public static int CountSubstring(this ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle)
{
return CountSubstring(haystack, needle, StringComparison.Ordinal);
}
/// <summary>
/// Counts the occurrences of a substring within the current character span, using a specified string comparison method.
/// </summary>
/// <param name="haystack">The haystack search space.</param>
/// <param name="needle">The character span to count.</param>
/// <param name="comparison">The string comparison method used for determining substring count.</param>
/// <returns>An integer representing the count of <paramref name="needle" /> inside <paramref name="haystack" />.</returns>
public static int CountSubstring(this ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle, StringComparison comparison)
{
if (haystack.IsEmpty || needle.IsEmpty)
{
return 0;
}
int haystackLength = haystack.Length;
int needleLength = needle.Length;
var count = 0;
for (var index = 0; index < haystackLength - needleLength - 1; index++)
{
if (haystack[index..(index + needleLength)].Equals(needle, comparison))
{
count++;
}
}
return count;
}
/// <summary>
/// Counts the occurrences of a substring within the current character span.
/// </summary>
/// <param name="haystack">The haystack search space.</param>
/// <param name="needle">The character span to count.</param>
/// <returns>An integer representing the count of <paramref name="needle" /> inside <paramref name="haystack" />.</returns>
public static int CountSubstring(this Span<char> haystack, Span<char> needle)
{
return CountSubstring(haystack, needle, StringComparison.Ordinal);
}
/// <summary>
/// Counts the occurrences of a substring within the current character span, using a specified string comparison method.
/// </summary>
/// <param name="haystack">The haystack search space.</param>
/// <param name="needle">The character span to count.</param>
/// <param name="comparison">The string comparison method used for determining substring count.</param>
/// <returns>An integer representing the count of <paramref name="needle" /> inside <paramref name="haystack" />.</returns>
public static int CountSubstring(this Span<char> haystack, Span<char> needle, StringComparison comparison)
{
return CountSubstring((ReadOnlySpan<char>)haystack, needle, comparison);
}
/// <summary>
/// Counts the occurrences of a substring within the current string.
/// </summary>