From 7a905b571a878b043760d33f082738e07e64fe8d Mon Sep 17 00:00:00 2001 From: Matthew Layton <9935122+MrMatthewLayton@users.noreply.github.com> Date: Sat, 11 Jan 2025 17:42:20 +0000 Subject: [PATCH] Implemented string extension methods for NthIndexOf, SubstringBeforeNth, and SubstringAfterNth, and tests. (#93) Implemented string extension methods for NthIndexOf, SubstringBeforeNth, and SubstringAfterNth, and tests. --- .../StringExtensionTests.cs | 98 ++++++++++ OnixLabs.Core/Extensions.String.cs | 168 +++++++++++++++++- OnixLabs.Playground/Program.cs | 8 - 3 files changed, 261 insertions(+), 13 deletions(-) diff --git a/OnixLabs.Core.UnitTests/StringExtensionTests.cs b/OnixLabs.Core.UnitTests/StringExtensionTests.cs index ce2fdbb..7cbd138 100644 --- a/OnixLabs.Core.UnitTests/StringExtensionTests.cs +++ b/OnixLabs.Core.UnitTests/StringExtensionTests.cs @@ -20,6 +20,38 @@ namespace OnixLabs.Core.UnitTests; public sealed class StringExtensionTests { + [Theory(DisplayName = "String.NthIndexOf should return the expected result")] + [InlineData("First:Second:Third:Fourth", ':', 0, -1)] + [InlineData("First:Second:Third:Fourth", ':', 1, 5)] + [InlineData("First:Second:Third:Fourth", ':', 2, 12)] + [InlineData("First:Second:Third:Fourth", ':', 3, 18)] + [InlineData("First:Second:Third:Fourth", ':', 4, -1)] + [InlineData("First:Second:Third:Fourth", ':', 100, -1)] + public void NthIndexOfCharShouldProduceExpectedResult(string value, char seekValue, int count, int expected) + { + // When + int actual = value.NthIndexOf(seekValue, count); + + // Then + Assert.Equal(expected, actual); + } + + [Theory(DisplayName = "String.NthIndexOf should return the expected result")] + [InlineData("First_split_Second_split_Third_split_Fourth", "_split_", 0, -1)] + [InlineData("First_split_Second_split_Third_split_Fourth", "_split_", 1, 5)] + [InlineData("First_split_Second_split_Third_split_Fourth", "_split_", 2, 18)] + [InlineData("First_split_Second_split_Third_split_Fourth", "_split_", 3, 30)] + [InlineData("First_split_Second_split_Third_split_Fourth", "_split_", 4, -1)] + [InlineData("First_split_Second_split_Third_split_Fourth", "_split_", 100, -1)] + public void NthIndexOfStringShouldProduceExpectedResult(string value, string seekValue, int count, int expected) + { + // When + int actual = value.NthIndexOf(seekValue, count); + + // Then + Assert.Equal(expected, actual); + } + [Theory(DisplayName = "String.Repeat should return the expected result")] [InlineData("0", 10, "0000000000")] [InlineData("Abc1", 3, "Abc1Abc1Abc1")] @@ -128,6 +160,72 @@ public void SubstringAfterLastShouldProduceExpectedResultString(string value, st Assert.Equal(expected, actual); } + [Theory(DisplayName = "String.SubstringBeforeNth(char) should return the expected substring or default value")] + [InlineData("One:Two:Three:Four", ':', 1, null, "One")] + [InlineData("One:Two:Three:Four", ':', 2, null, "One:Two")] + [InlineData("One:Two:Three:Four", ':', 3, null, "One:Two:Three")] + [InlineData("One:Two:Three:Four", ':', 4, null, "One:Two:Three:Four")] + [InlineData("One:Two:Three:Four", ':', 4, "NOT_FOUND", "NOT_FOUND")] + [InlineData("One:Two:Three", ':', 0, null, "One:Two:Three")] + [InlineData("One:Two:Three", ':', -1, null, "One:Two:Three")] + public void SubstringBeforeNthCharShouldProduceExpectedResult(string value, char delimiter, int nth, string? defaultValue, string expected) + { + // When + string actual = value.SubstringBeforeNth(delimiter, nth, defaultValue); + + // Then + Assert.Equal(expected, actual); + } + + [Theory(DisplayName = "String.SubstringBeforeNth(string) should return the expected substring or default value")] + [InlineData("One_split_Two_split_Three_split_Four", "_split_", 1, null, "One")] + [InlineData("One_split_Two_split_Three_split_Four", "_split_", 2, null, "One_split_Two")] + [InlineData("One_split_Two_split_Three_split_Four", "_split_", 3, null, "One_split_Two_split_Three")] + [InlineData("One_split_Two_split_Three_split_Four", "_split_", 4, null, "One_split_Two_split_Three_split_Four")] + [InlineData("One_split_Two_split_Three_split_Four", "_split_", 4, "NOT_FOUND", "NOT_FOUND")] + [InlineData("NoDelimiterHere", "_split_", 1, null, "NoDelimiterHere")] + public void SubstringBeforeNthStringShouldProduceExpectedResult(string value, string delimiter, int nth, string? defaultValue, string expected) + { + // When + string actual = value.SubstringBeforeNth(delimiter, nth, defaultValue); + + // Then + Assert.Equal(expected, actual); + } + + [Theory(DisplayName = "String.SubstringAfterNth(char) should return the expected substring or default value")] + [InlineData("One:Two:Three:Four", ':', 1, null, "Two:Three:Four")] + [InlineData("One:Two:Three:Four", ':', 2, null, "Three:Four")] + [InlineData("One:Two:Three:Four", ':', 3, null, "Four")] + [InlineData("One:Two:Three:Four", ':', 4, null, "One:Two:Three:Four")] + [InlineData("One:Two:Three:Four", ':', 4, "NOT_FOUND", "NOT_FOUND")] + [InlineData("One:Two:Three", ':', 0, null, "One:Two:Three")] + [InlineData("One:Two:Three", ':', -1, null, "One:Two:Three")] + public void SubstringAfterNthCharShouldProduceExpectedResult(string value, char delimiter, int nth, string? defaultValue, string expected) + { + // When + string actual = value.SubstringAfterNth(delimiter, nth, defaultValue); + + // Then + Assert.Equal(expected, actual); + } + + [Theory(DisplayName = "String.SubstringAfterNth(string) should return the expected substring or default value")] + [InlineData("One_split_Two_split_Three_split_Four", "_split_", 1, null, "Two_split_Three_split_Four")] + [InlineData("One_split_Two_split_Three_split_Four", "_split_", 2, null, "Three_split_Four")] + [InlineData("One_split_Two_split_Three_split_Four", "_split_", 3, null, "Four")] + [InlineData("One_split_Two_split_Three_split_Four", "_split_", 4, null, "One_split_Two_split_Three_split_Four")] + [InlineData("One_split_Two_split_Three_split_Four", "_split_", 4, "NOT_FOUND", "NOT_FOUND")] + [InlineData("NoDelimiterHere", "_split_", 1, null, "NoDelimiterHere")] + public void SubstringAfterNthStringShouldProduceExpectedResult(string value, string delimiter, int nth, string? defaultValue, string expected) + { + // When + string actual = value.SubstringAfterNth(delimiter, nth, defaultValue); + + // Then + Assert.Equal(expected, actual); + } + [Theory(DisplayName = "String.ToByteArray should produce the byte array equivalent of the current string")] [InlineData("Hello, World!", new byte[] { 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21 })] public void ToByteArrayShouldProduceExpectedResult(string value, byte[] expected) diff --git a/OnixLabs.Core/Extensions.String.cs b/OnixLabs.Core/Extensions.String.cs index f0d0a25..adccb0b 100644 --- a/OnixLabs.Core/Extensions.String.cs +++ b/OnixLabs.Core/Extensions.String.cs @@ -42,13 +42,78 @@ public static class StringExtensions /// private const DateTimeStyles DefaultStyles = DateTimeStyles.None; + /// + /// Obtains the zero-based index of the nth occurrence of the specified character in this instance. + /// If the specified occurrence does not exist, returns -1. + /// + /// The string to search. + /// The character to seek. + /// The number of occurrences to skip before returning an index. + /// + /// Returns the zero-based index position of the nth occurrence of , if found; otherwise, -1. + /// + public static int NthIndexOf(this string value, char seekValue, int count) + { + if (string.IsNullOrEmpty(value) || count <= 0) return -1; + + int occurrences = 0; + + for (int i = 0; i < value.Length; i++) + { + if (value[i] != seekValue) continue; + + occurrences++; + + if (occurrences != count) continue; + + return i; + } + + return NotFound; + } + + /// + /// Obtains the zero-based index of the nth occurrence of the specified character in this instance. + /// If the specified occurrence does not exist, returns -1. + /// + /// The string to search. + /// The substring to seek. + /// The number of occurrences to skip before returning an index. + /// The comparison that will be used to compare the current value and the seek value. + /// + /// Returns the zero-based index position of the nth occurrence of , if found; otherwise, -1. + /// + public static int NthIndexOf(this string value, string seekValue, int count, StringComparison comparison = DefaultComparison) + { + if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(seekValue) || count <= 0) return -1; + + int occurrences = 0; + int startIndex = 0; + + while (true) + { + int index = value.IndexOf(seekValue, startIndex, comparison); + + if (index == -1) return -1; + + occurrences++; + + if (occurrences == count) return index; + + startIndex = index + seekValue.Length; + + if (startIndex >= value.Length) return NotFound; + } + } + /// /// Repeats the current by the specified number of repetitions. /// /// The instance to repeat. /// The number of repetitions of the current instance. /// Returns a new instance representing the repetition of the current instance. - public static string Repeat(this string value, int count) => count > 0 ? string.Join(string.Empty, Enumerable.Repeat(value, count)) : string.Empty; + public static string Repeat(this string value, int count) => + count > 0 ? string.Join(string.Empty, Enumerable.Repeat(value, count)) : string.Empty; /// /// Obtains a sub-string before the specified index within the current instance. @@ -64,7 +129,8 @@ public static class StringExtensions /// If the default value is , then the current instance will be returned. /// // ReSharper disable once HeapView.ObjectAllocation - private static string SubstringBeforeIndex(this string value, int index, string? defaultValue = null) => index <= NotFound ? defaultValue ?? value : value[..index]; + private static string SubstringBeforeIndex(this string value, int index, string? defaultValue = null) => + index <= NotFound ? defaultValue ?? value : value[..index]; /// /// Obtains a sub-string after the specified index within the current instance. @@ -81,7 +147,8 @@ public static class StringExtensions /// If the default value is , then the current instance will be returned. /// // ReSharper disable once HeapView.ObjectAllocation - private static string SubstringAfterIndex(this string value, int index, int offset, string? defaultValue = null) => index <= NotFound ? defaultValue ?? value : value[(index + offset)..value.Length]; + private static string SubstringAfterIndex(this string value, int index, int offset, string? defaultValue = null) => + index <= NotFound ? defaultValue ?? value : value[(index + offset)..value.Length]; /// /// Obtains a sub-string before the first occurrence of the specified delimiter within the current instance. @@ -235,6 +302,95 @@ public static string SubstringAfterLast(this string value, char delimiter, strin public static string SubstringAfterLast(this string value, string delimiter, string? defaultValue = null, StringComparison comparison = DefaultComparison) => value.SubstringAfterIndex(value.LastIndexOf(delimiter, comparison), 1, defaultValue); + /// + /// Obtains a sub-string before the nth occurrence of the specified character within the current instance. + /// If the nth occurrence is not found, returns the or the entire string if default is null. + /// + /// The current instance from which to obtain a sub-string. + /// The character to find the nth occurrence of. + /// The nth occurrence to find. + /// + /// The value to return if the nth occurrence is not found. + /// If the default value is , the current instance is returned. + /// + /// + /// A sub-string before the nth occurrence of if found; otherwise, + /// or the entire string if default is null. + /// + public static string SubstringBeforeNth(this string value, char seekValue, int count, string? defaultValue = null) + { + int index = value.NthIndexOf(seekValue, count); + return value.SubstringBeforeIndex(index, defaultValue); + } + + /// + /// Obtains a sub-string before the nth occurrence of the specified substring within the current instance. + /// If the nth occurrence is not found, returns the or the entire string if default is null. + /// + /// The current instance from which to obtain a sub-string. + /// The substring to find the nth occurrence of. + /// The nth occurrence to find. + /// + /// The value to return if the nth occurrence is not found. + /// If the default value is , the current instance is returned. + /// + /// The comparison that will be used to compare the current value and the seek value. + /// + /// A sub-string before the nth occurrence of if found; otherwise, + /// or the entire string if default is null. + /// + public static string SubstringBeforeNth(this string value, string seekValue, int count, string? defaultValue = null, StringComparison comparison = DefaultComparison) + { + int index = value.NthIndexOf(seekValue, count, comparison); + return value.SubstringBeforeIndex(index, defaultValue); + } + + /// + /// Obtains a sub-string after the nth occurrence of the specified character within the current instance. + /// If the nth occurrence is not found, returns the or the entire string if default is null. + /// + /// The current instance from which to obtain a sub-string. + /// The character to find the nth occurrence of. + /// The nth occurrence to find. + /// + /// The value to return if the nth occurrence is not found. + /// If the default value is , the current instance is returned. + /// + /// + /// A sub-string after the nth occurrence of if found; otherwise, + /// or the entire string if default is null. + /// + public static string SubstringAfterNth(this string value, char seekValue, int count, string? defaultValue = null) + { + int index = value.NthIndexOf(seekValue, count); + // Move 1 character after the nth occurrence index. + return value.SubstringAfterIndex(index, 1, defaultValue); + } + + /// + /// Obtains a sub-string after the nth occurrence of the specified substring within the current instance. + /// If the nth occurrence is not found, returns the or the entire string if default is null. + /// + /// The current instance from which to obtain a sub-string. + /// The substring to find the nth occurrence of. + /// The nth occurrence to find. + /// + /// The value to return if the nth occurrence is not found. + /// If the default value is , the current instance is returned. + /// + /// The comparison that will be used to compare the current value and the seek value. + /// + /// A sub-string after the nth occurrence of if found; otherwise, + /// or the entire string if default is null. + /// + public static string SubstringAfterNth(this string value, string seekValue, int count, string? defaultValue = null, StringComparison comparison = DefaultComparison) + { + int index = value.NthIndexOf(seekValue, count, comparison); + // Move by the length of the found substring after the nth occurrence index. + int offset = (index != NotFound && !string.IsNullOrEmpty(seekValue)) ? seekValue.Length : 0; + return value.SubstringAfterIndex(index, offset, defaultValue); + } + /// /// Converts the current instance into a new instance. /// @@ -336,7 +492,8 @@ public static bool TryCopyTo(this string value, Span destination, out int /// The that should precede the current instance. /// The that should succeed the current instance. /// Returns a new instance representing the current instance, wrapped between the specified before and after instances. - public static string Wrap(this string value, char before, char after) => string.Concat(before.ToString(), value, after.ToString()); + public static string Wrap(this string value, char before, char after) => + string.Concat(before.ToString(), value, after.ToString()); /// /// Wraps the current instance between the specified before and after instances. @@ -345,5 +502,6 @@ public static bool TryCopyTo(this string value, Span destination, out int /// The that should precede the current instance. /// The that should succeed the current instance. /// Returns a new instance representing the current instance, wrapped between the specified before and after instances. - public static string Wrap(this string value, string before, string after) => string.Concat(before, value, after); + public static string Wrap(this string value, string before, string after) => + string.Concat(before, value, after); } diff --git a/OnixLabs.Playground/Program.cs b/OnixLabs.Playground/Program.cs index 55fdc74..d629839 100644 --- a/OnixLabs.Playground/Program.cs +++ b/OnixLabs.Playground/Program.cs @@ -12,19 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using OnixLabs.Security.Cryptography; - namespace OnixLabs.Playground; internal static class Program { private static void Main() { - string value = "SHA256:043a718774c572bd8a25adbeb1bfcd5c0256ae11cecf9f9c3f925d0e52beaf89"; - - NamedHash hash = NamedHash.Parse(value); - - Console.WriteLine(hash); } }