Skip to content

Commit

Permalink
Implemented string extension methods for NthIndexOf, SubstringBeforeN…
Browse files Browse the repository at this point in the history
…th, and SubstringAfterNth, and tests. (#93)

Implemented string extension methods for NthIndexOf, SubstringBeforeNth, and SubstringAfterNth, and tests.
  • Loading branch information
MrMatthewLayton authored Jan 11, 2025
1 parent f5b4658 commit 7a905b5
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 13 deletions.
98 changes: 98 additions & 0 deletions OnixLabs.Core.UnitTests/StringExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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)
Expand Down
168 changes: 163 additions & 5 deletions OnixLabs.Core/Extensions.String.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,78 @@ public static class StringExtensions
/// </summary>
private const DateTimeStyles DefaultStyles = DateTimeStyles.None;

/// <summary>
/// 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.
/// </summary>
/// <param name="value">The string to search.</param>
/// <param name="seekValue">The character to seek.</param>
/// <param name="count">The number of occurrences to skip before returning an index.</param>
/// <returns>
/// Returns the zero-based index position of the nth occurrence of <paramref name="seekValue"/>, if found; otherwise, -1.
/// </returns>
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;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="value">The string to search.</param>
/// <param name="seekValue">The substring to seek.</param>
/// <param name="count">The number of occurrences to skip before returning an index.</param>
/// <param name="comparison">The comparison that will be used to compare the current value and the seek value.</param>
/// <returns>
/// Returns the zero-based index position of the nth occurrence of <paramref name="seekValue"/>, if found; otherwise, -1.
/// </returns>
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;
}
}

/// <summary>
/// Repeats the current <see cref="String"/> by the specified number of repetitions.
/// </summary>
/// <param name="value">The <see cref="String"/> instance to repeat.</param>
/// <param name="count">The number of repetitions of the current <see cref="String"/> instance.</param>
/// <returns>Returns a new <see cref="String"/> instance representing the repetition of the current <see cref="String"/> instance.</returns>
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;

/// <summary>
/// Obtains a sub-string before the specified index within the current <see cref="String"/> instance.
Expand All @@ -64,7 +129,8 @@ public static class StringExtensions
/// If the default value is <see langword="null"/>, then the current <see cref="String"/> instance will be returned.
/// </returns>
// 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];

/// <summary>
/// Obtains a sub-string after the specified index within the current <see cref="String"/> instance.
Expand All @@ -81,7 +147,8 @@ public static class StringExtensions
/// If the default value is <see langword="null"/>, then the current <see cref="String"/> instance will be returned.
/// </returns>
// 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];

/// <summary>
/// Obtains a sub-string before the first occurrence of the specified delimiter within the current <see cref="String"/> instance.
Expand Down Expand Up @@ -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);

/// <summary>
/// Obtains a sub-string before the nth occurrence of the specified character within the current <see cref="String"/> instance.
/// If the nth occurrence is not found, returns the <paramref name="defaultValue"/> or the entire string if default is null.
/// </summary>
/// <param name="value">The current <see cref="String"/> instance from which to obtain a sub-string.</param>
/// <param name="seekValue">The character to find the nth occurrence of.</param>
/// <param name="count">The nth occurrence to find.</param>
/// <param name="defaultValue">
/// The <see cref="String"/> value to return if the nth occurrence is not found.
/// If the default value is <see langword="null"/>, the current <see cref="String"/> instance is returned.
/// </param>
/// <returns>
/// A sub-string before the nth occurrence of <paramref name="seekValue"/> if found; otherwise,
/// <paramref name="defaultValue"/> or the entire string if default is null.
/// </returns>
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);
}

/// <summary>
/// Obtains a sub-string before the nth occurrence of the specified substring within the current <see cref="String"/> instance.
/// If the nth occurrence is not found, returns the <paramref name="defaultValue"/> or the entire string if default is null.
/// </summary>
/// <param name="value">The current <see cref="String"/> instance from which to obtain a sub-string.</param>
/// <param name="seekValue">The substring to find the nth occurrence of.</param>
/// <param name="count">The nth occurrence to find.</param>
/// <param name="defaultValue">
/// The <see cref="String"/> value to return if the nth occurrence is not found.
/// If the default value is <see langword="null"/>, the current <see cref="String"/> instance is returned.
/// </param>
/// <param name="comparison">The comparison that will be used to compare the current value and the seek value.</param>
/// <returns>
/// A sub-string before the nth occurrence of <paramref name="seekValue"/> if found; otherwise,
/// <paramref name="defaultValue"/> or the entire string if default is null.
/// </returns>
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);
}

/// <summary>
/// Obtains a sub-string after the nth occurrence of the specified character within the current <see cref="String"/> instance.
/// If the nth occurrence is not found, returns the <paramref name="defaultValue"/> or the entire string if default is null.
/// </summary>
/// <param name="value">The current <see cref="String"/> instance from which to obtain a sub-string.</param>
/// <param name="seekValue">The character to find the nth occurrence of.</param>
/// <param name="count">The nth occurrence to find.</param>
/// <param name="defaultValue">
/// The <see cref="String"/> value to return if the nth occurrence is not found.
/// If the default value is <see langword="null"/>, the current <see cref="String"/> instance is returned.
/// </param>
/// <returns>
/// A sub-string after the nth occurrence of <paramref name="seekValue"/> if found; otherwise,
/// <paramref name="defaultValue"/> or the entire string if default is null.
/// </returns>
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);
}

/// <summary>
/// Obtains a sub-string after the nth occurrence of the specified substring within the current <see cref="String"/> instance.
/// If the nth occurrence is not found, returns the <paramref name="defaultValue"/> or the entire string if default is null.
/// </summary>
/// <param name="value">The current <see cref="String"/> instance from which to obtain a sub-string.</param>
/// <param name="seekValue">The substring to find the nth occurrence of.</param>
/// <param name="count">The nth occurrence to find.</param>
/// <param name="defaultValue">
/// The <see cref="String"/> value to return if the nth occurrence is not found.
/// If the default value is <see langword="null"/>, the current <see cref="String"/> instance is returned.
/// </param>
/// <param name="comparison">The comparison that will be used to compare the current value and the seek value.</param>
/// <returns>
/// A sub-string after the nth occurrence of <paramref name="seekValue"/> if found; otherwise,
/// <paramref name="defaultValue"/> or the entire string if default is null.
/// </returns>
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);
}

/// <summary>
/// Converts the current <see cref="String"/> instance into a new <see cref="T:Byte[]"/> instance.
/// </summary>
Expand Down Expand Up @@ -336,7 +492,8 @@ public static bool TryCopyTo(this string value, Span<char> destination, out int
/// <param name="before">The <see cref="Char"/> that should precede the current <see cref="String"/> instance.</param>
/// <param name="after">The <see cref="Char"/> that should succeed the current <see cref="String"/> instance.</param>
/// <returns>Returns a new <see cref="String"/> instance representing the current <see cref="String"/> instance, wrapped between the specified before and after <see cref="Char"/> instances.</returns>
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());

/// <summary>
/// Wraps the current <see cref="String"/> instance between the specified before and after <see cref="String"/> instances.
Expand All @@ -345,5 +502,6 @@ public static bool TryCopyTo(this string value, Span<char> destination, out int
/// <param name="before">The <see cref="String"/> that should precede the current <see cref="String"/> instance.</param>
/// <param name="after">The <see cref="String"/> that should succeed the current <see cref="String"/> instance.</param>
/// <returns>Returns a new <see cref="String"/> instance representing the current <see cref="String"/> instance, wrapped between the specified before and after <see cref="String"/> instances.</returns>
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);
}
8 changes: 0 additions & 8 deletions OnixLabs.Playground/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

0 comments on commit 7a905b5

Please sign in to comment.