From 53e8b2ff64c78f00550e49c02e71fd98449d61aa Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Tue, 29 Nov 2022 12:39:34 +0000 Subject: [PATCH] Repurpose Span.Split to accept generic --- CHANGELOG.md | 6 +- X10D.Tests/src/Collections/SpanTest.cs | 91 +++++++++++ X10D.Tests/src/Text/CharSpanTests.cs | 49 ------ X10D/src/Collections/SpanExtensions.cs | 200 +++++++++++++++++++++++++ X10D/src/Text/CharSpanExtensions.cs | 70 --------- 5 files changed, 295 insertions(+), 121 deletions(-) create mode 100644 X10D.Tests/src/Collections/SpanTest.cs create mode 100644 X10D/src/Collections/SpanExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e02d1c9..5e2d763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,14 +30,16 @@ - 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 `ReadOnlySpan.Split(T)` +- X10D: Added `ReadOnlySpan.Split(ReadOnlySpan)` - 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 `Span.Split(T)` +- X10D: Added `Span.Split(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/Collections/SpanTest.cs b/X10D.Tests/src/Collections/SpanTest.cs new file mode 100644 index 0000000..41f3809 --- /dev/null +++ b/X10D.Tests/src/Collections/SpanTest.cs @@ -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 span = ReadOnlySpan.Empty; + + var index = 0; + foreach (ReadOnlySpan unused in span.Split(' ')) + { + index++; + } + + Assert.AreEqual(0, index); + } + + [TestMethod] + public void Split_OnOneWord_ShouldYieldLength1() + { + ReadOnlySpan span = "Hello ".AsSpan(); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(' ')) + { + if (index == 0) + { + Assert.AreEqual("Hello", subSpan.ToString()); + } + + index++; + } + + Assert.AreEqual(1, index); + } + + [TestMethod] + public void Split_OnTwoWords_ShouldYieldLength2() + { + ReadOnlySpan span = "Hello World ".AsSpan(); + + var index = 0; + foreach (ReadOnlySpan 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 span = "Hello, the World ".AsSpan(); + + var index = 0; + foreach (ReadOnlySpan 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); + } +} diff --git a/X10D.Tests/src/Text/CharSpanTests.cs b/X10D.Tests/src/Text/CharSpanTests.cs index 192989d..6283841 100644 --- a/X10D.Tests/src/Text/CharSpanTests.cs +++ b/X10D.Tests/src/Text/CharSpanTests.cs @@ -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 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/src/Collections/SpanExtensions.cs b/X10D/src/Collections/SpanExtensions.cs new file mode 100644 index 0000000..40e855c --- /dev/null +++ b/X10D/src/Collections/SpanExtensions.cs @@ -0,0 +1,200 @@ +namespace X10D.Collections; + +/// +/// Extension methods for and +/// +public static class SpanExtensions +{ + /// + /// Returns the number of times that a specified element appears in a span of elements of the same type. + /// + /// The source to search. + /// The element to count. + /// The type of elements in . + /// The number of times that appears in . + public static int Count(this in Span source, T element) + where T : IEquatable + { + return source.AsReadOnly().Count(element); + } + + /// + /// Returns the number of times that a specified element appears in a read-only span of elements of the same type. + /// + /// The source to search. + /// The element to count. + /// The type of elements in . + /// The number of times that appears in . + public static int Count(this in ReadOnlySpan source, T element) + where T : IEquatable + { + var count = 0; + + foreach (T item in source) + { + if (item.Equals(element)) + { + count++; + } + } + + return count; + } + + /// + /// Returns a read-only wrapper for the current span. + /// + /// The source span. + /// The type of elements in . + /// A which wraps the elements in . + public static ReadOnlySpan AsReadOnly(this in Span source) + { + return source; + } + + /// + /// Splits a span of elements into sub-spans based on a delimiting element. + /// + /// The span to split. + /// The delimiting element. + /// The type of elements in . + /// + /// An enumerator which wraps and delimits the elements based on . + /// + public static SpanSplitEnumerator Split(this in Span source, T delimiter) + where T : struct, IEquatable + { + return source.AsReadOnly().Split(delimiter); + } + + /// + /// Splits a span of elements into sub-spans based on a delimiting element. + /// + /// The span to split. + /// The delimiting element. + /// The type of elements in . + /// + /// An enumerator which wraps and delimits the elements based on . + /// + public static SpanSplitEnumerator Split(this in ReadOnlySpan source, T delimiter) + where T : struct, IEquatable + { + return new SpanSplitEnumerator(source, delimiter); + } + + /// + /// Splits a span of elements into sub-spans based on a span of delimiting elements. + /// + /// The span to split. + /// The span of delimiting elements. + /// The type of elements in . + /// + /// An enumerator which wraps and delimits the elements based on . + /// + public static SpanSplitEnumerator Split(this in Span source, in ReadOnlySpan delimiter) + where T : struct, IEquatable + { + return source.AsReadOnly().Split(delimiter); + } + + /// + /// Splits a span of elements into sub-spans based on a span of delimiting elements. + /// + /// The span to split. + /// The span of delimiting elements. + /// The type of elements in . + /// + /// An enumerator which wraps and delimits the elements based on . + /// + public static SpanSplitEnumerator Split(this in ReadOnlySpan source, in ReadOnlySpan delimiter) + where T : struct, IEquatable + { + return new SpanSplitEnumerator(source, delimiter); + } + + /// + /// Enumerates the elements of a . + /// + /// The type of elements in the span. + public ref struct SpanSplitEnumerator where T : struct, IEquatable + { + private ReadOnlySpan _source; + private readonly ReadOnlySpan _delimiterSpan; + private readonly T _delimiter; + private readonly bool _usingSpanDelimiter; + + /// + /// Initializes a new instance of the struct. + /// + /// The source span. + /// The delimiting span of elements. + public SpanSplitEnumerator(in ReadOnlySpan source, ReadOnlySpan delimiter) + { + _usingSpanDelimiter = true; + _source = source; + _delimiter = default; + _delimiterSpan = delimiter; + Current = ReadOnlySpan.Empty; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The source span. + /// The delimiting element. + public SpanSplitEnumerator(in ReadOnlySpan source, T delimiter) + { + _usingSpanDelimiter = false; + _source = source; + _delimiter = delimiter; + _delimiterSpan = ReadOnlySpan.Empty; + Current = ReadOnlySpan.Empty; + } + + /// + /// Gets the element at the current position of the enumerator. + /// + /// The element in the at the current position of the enumerator. + public ReadOnlySpan Current { get; private set; } + + /// + /// Returns the current enumerator. + /// + /// The current instance of . + /// + /// This method exists to provide the ability to enumerate within a foreach loop. It should not be called + /// manually. + /// + public readonly SpanSplitEnumerator GetEnumerator() + { + return this; + } + + /// + /// Advances the enumerator to the next element of the . + /// + /// + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the span. + /// + 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.Empty; + return true; + } + + Current = _source[..index]; + _source = _source[(index + 1)..]; + return true; + } + } +} diff --git a/X10D/src/Text/CharSpanExtensions.cs b/X10D/src/Text/CharSpanExtensions.cs index 603a50f..b5c0deb 100644 --- a/X10D/src/Text/CharSpanExtensions.cs +++ b/X10D/src/Text/CharSpanExtensions.cs @@ -67,74 +67,4 @@ public static class CharSpanExtensions 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; - } }