From ed8651172b3760cafca3d08e59239d9762bf583f Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Mon, 28 Nov 2022 02:59:46 +0000 Subject: [PATCH] Add [ReadOnly]Span.Split This commit also migrates CountSubstring from StringExtensions to CharSpanExtensions --- CHANGELOG.md | 2 + X10D.Tests/src/Text/CharSpanTests.cs | 88 +++++++++++++++++ X10D.Tests/src/Text/StringTests.cs | 8 -- X10D/src/Text/CharSpanExtensions.cs | 140 +++++++++++++++++++++++++++ X10D/src/Text/StringExtensions.cs | 63 ------------ 5 files changed, 230 insertions(+), 71 deletions(-) create mode 100644 X10D.Tests/src/Text/CharSpanTests.cs create mode 100644 X10D/src/Text/CharSpanExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e1f6c5..e02d1c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,12 +30,14 @@ - X10D: Added `PopCount()` for built-in integer types - X10D: Added `ReadOnlySpan.CountSubstring(char)` - X10D: Added `ReadOnlySpan.CountSubstring(ReadOnlySpan[, StringComparison])` +- X10D: Added `ReadOnlySpan.Split(ReadOnlySpan[, StringComparison])` - X10D: Added `RoundUpToPowerOf2()` for built-in integer types - X10D: Added `Size.ToPoint()` - X10D: Added `Size.ToPointF()` - X10D: Added `Size.ToVector2()` - X10D: Added `Span.CountSubstring(char)` - X10D: Added `Span.CountSubstring(Span[, StringComparison])` +- X10D: Added `Span.Split(char, Span)` - 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 diff --git a/X10D.Tests/src/Text/CharSpanTests.cs b/X10D.Tests/src/Text/CharSpanTests.cs new file mode 100644 index 0000000..192989d --- /dev/null +++ b/X10D.Tests/src/Text/CharSpanTests.cs @@ -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 span = ReadOnlySpan.Empty; + Assert.AreEqual(0, span.Split(' ', Span.Empty)); + } + + [TestMethod] + public void Split_OnOneWord_ShouldYieldLength1() + { + ReadOnlySpan span = "Hello".AsSpan(); + Span 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 span = "Hello World".AsSpan(); + Span 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 span = "Hello, the World".AsSpan(); + Span 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()); + } +} diff --git a/X10D.Tests/src/Text/StringTests.cs b/X10D.Tests/src/Text/StringTests.cs index 384c8b8..529124a 100644 --- a/X10D.Tests/src/Text/StringTests.cs +++ b/X10D.Tests/src/Text/StringTests.cs @@ -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] diff --git a/X10D/src/Text/CharSpanExtensions.cs b/X10D/src/Text/CharSpanExtensions.cs new file mode 100644 index 0000000..603a50f --- /dev/null +++ b/X10D/src/Text/CharSpanExtensions.cs @@ -0,0 +1,140 @@ +namespace X10D.Text; + +/// +/// Extension methods for and of . +/// +public static class CharSpanExtensions +{ + /// + /// Counts the occurrences of a substring within the current character span. + /// + /// The haystack search space. + /// The character span to count. + /// An integer representing the count of inside . + public static int CountSubstring(this Span haystack, Span needle) + { + return CountSubstring(haystack, needle, StringComparison.Ordinal); + } + + /// + /// Counts the occurrences of a substring within the current character span, using a specified string comparison method. + /// + /// The haystack search space. + /// The character span to count. + /// The string comparison method used for determining substring count. + /// An integer representing the count of inside . + public static int CountSubstring(this Span haystack, Span needle, StringComparison comparison) + { + return CountSubstring((ReadOnlySpan)haystack, needle, comparison); + } + + /// + /// Counts the occurrences of a substring within the current character span. + /// + /// The haystack search space. + /// The character span to count. + /// An integer representing the count of inside . + public static int CountSubstring(this ReadOnlySpan haystack, ReadOnlySpan needle) + { + return CountSubstring(haystack, needle, StringComparison.Ordinal); + } + + /// + /// Counts the occurrences of a substring within the current character span, using a specified string comparison method. + /// + /// The haystack search space. + /// The character span to count. + /// The string comparison method used for determining substring count. + /// An integer representing the count of inside . + public static int CountSubstring(this ReadOnlySpan haystack, ReadOnlySpan 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; + } + + /// + /// Splits a span of characters into substrings based on a specific delimiting character. + /// + /// The span of characters to split. + /// A character that delimits the substring in this character span. + /// + /// When this method returns, will be populated with the values pointing to where each substring + /// starts and ends in . + /// + /// + /// The number of substrings within . This value is always correct regardless of the length of + /// . + /// + public static int Split(this Span value, char separator, Span destination) + { + return ((ReadOnlySpan)value).Split(separator, destination); + } + + /// + /// Splits a span of characters into substrings based on a specific delimiting character. + /// + /// The span of characters to split. + /// A character that delimits the substring in this character span. + /// + /// When this method returns, will be populated with the values pointing to where each substring + /// starts and ends in . + /// + /// + /// The number of substrings within . This value is always correct regardless of the length of + /// . + /// + public static int Split(this ReadOnlySpan value, char separator, Span destination) + { + Span 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; + } +} diff --git a/X10D/src/Text/StringExtensions.cs b/X10D/src/Text/StringExtensions.cs index df1fbf2..64dc85c 100644 --- a/X10D/src/Text/StringExtensions.cs +++ b/X10D/src/Text/StringExtensions.cs @@ -214,69 +214,6 @@ public static class StringExtensions return haystack.AsSpan().CountSubstring(needle); } - /// - /// Counts the occurrences of a substring within the current character span. - /// - /// The haystack search space. - /// The character span to count. - /// An integer representing the count of inside . - public static int CountSubstring(this ReadOnlySpan haystack, ReadOnlySpan needle) - { - return CountSubstring(haystack, needle, StringComparison.Ordinal); - } - - /// - /// Counts the occurrences of a substring within the current character span, using a specified string comparison method. - /// - /// The haystack search space. - /// The character span to count. - /// The string comparison method used for determining substring count. - /// An integer representing the count of inside . - public static int CountSubstring(this ReadOnlySpan haystack, ReadOnlySpan 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; - } - - /// - /// Counts the occurrences of a substring within the current character span. - /// - /// The haystack search space. - /// The character span to count. - /// An integer representing the count of inside . - public static int CountSubstring(this Span haystack, Span needle) - { - return CountSubstring(haystack, needle, StringComparison.Ordinal); - } - - /// - /// Counts the occurrences of a substring within the current character span, using a specified string comparison method. - /// - /// The haystack search space. - /// The character span to count. - /// The string comparison method used for determining substring count. - /// An integer representing the count of inside . - public static int CountSubstring(this Span haystack, Span needle, StringComparison comparison) - { - return CountSubstring((ReadOnlySpan)haystack, needle, comparison); - } - /// /// Counts the occurrences of a substring within the current string. ///