diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eed9b7..6e1f6c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,10 +28,16 @@ - X10D: Added `PointF.ToSizeF()` - X10D: Added `PointF.ToVector2()` for .NET < 6 - X10D: Added `PopCount()` for built-in integer types +- X10D: Added `ReadOnlySpan.CountSubstring(char)` +- X10D: Added `ReadOnlySpan.CountSubstring(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 `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 - X10D: Added `Vector2.Deconstruct()` - X10D: Added `Vector2.IsOnLine(LineF)`, `Vector2.IsOnLine(PointF, PointF)`, and `Vector2.IsOnLine(Vector2, Vector2)` diff --git a/X10D.Tests/src/Text/StringTests.cs b/X10D.Tests/src/Text/StringTests.cs index fa99f40..384c8b8 100644 --- a/X10D.Tests/src/Text/StringTests.cs +++ b/X10D.Tests/src/Text/StringTests.cs @@ -101,6 +101,55 @@ public class StringTests Assert.ThrowsException(() => "Hello World".ChangeEncoding(Encoding.UTF8, null!)); } + [TestMethod] + public void CountSubstring_ShouldHonor_StringComparison() + { + 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] + public void CountSubstring_ShouldReturn0_GivenNoInstanceChar() + { + 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] + public void CountSubstring_ShouldReturn1_GivenSingleInstanceChar() + { + 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] + public void CountSubstring_ShouldReturn0_GivenEmptyHaystack() + { + 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] + public void CountSubstring_ShouldThrow_GivenNullHaystack() + { + Assert.ThrowsException(() => ((string?)null!).CountSubstring('\0')); + Assert.ThrowsException(() => ((string?)null!).CountSubstring(string.Empty)); + Assert.ThrowsException(() => + ((string?)null!).CountSubstring(string.Empty, StringComparison.OrdinalIgnoreCase)); + } + [TestMethod] public void EnumParse_ShouldReturnCorrectValue_GivenString() { diff --git a/X10D/src/Text/StringExtensions.cs b/X10D/src/Text/StringExtensions.cs index b7ffa57..1eb544f 100644 --- a/X10D/src/Text/StringExtensions.cs +++ b/X10D/src/Text/StringExtensions.cs @@ -162,6 +162,159 @@ public static class StringExtensions return value.GetBytes(sourceEncoding).ToString(destinationEncoding); } + /// + /// Counts the occurrences of a character within the current character span. + /// + /// The haystack search space. + /// The character to count. + /// An integer representing the count of inside . + public static int CountSubstring(this Span haystack, char needle) + { + return CountSubstring((ReadOnlySpan)haystack, needle); + } + + /// + /// Counts the occurrences of a character within the current character span. + /// + /// The haystack search space. + /// The character to count. + /// An integer representing the count of inside . + public static int CountSubstring(this ReadOnlySpan haystack, char needle) + { + var count = 0; + + for (var index = 0; index < haystack.Length; index++) + { + if (haystack[index] == needle) + { + count++; + } + } + + return count; + } + + /// + /// Counts the occurrences of a character within the current string. + /// + /// The haystack search space. + /// The character to count. + /// An integer representing the count of inside . + public static int CountSubstring(this string haystack, char needle) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(haystack); +#else + if (haystack is null) + { + throw new ArgumentNullException(nameof(haystack)); + } +#endif + + 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. + /// + /// The haystack search space. + /// The substring to count. + /// An integer representing the count of inside . + public static int CountSubstring(this string haystack, string? needle) + { + return CountSubstring(haystack, needle, StringComparison.Ordinal); + } + + /// + /// Counts the occurrences of a substring within the current string, using a specified string comparison method. + /// + /// The haystack search space. + /// The substring to count. + /// The string comparison method used for determining substring count. + /// An integer representing the count of inside . + public static int CountSubstring(this string haystack, string? needle, StringComparison comparison) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(haystack); +#else + if (haystack is null) + { + throw new ArgumentNullException(nameof(haystack)); + } +#endif + + if (string.IsNullOrWhiteSpace(needle)) + { + return 0; + } + + return haystack.AsSpan().CountSubstring(needle, comparison); + } + /// /// Parses a into an . ///