Skip to content

Commit 7a905b5

Browse files
Implemented string extension methods for NthIndexOf, SubstringBeforeNth, and SubstringAfterNth, and tests. (#93)
Implemented string extension methods for NthIndexOf, SubstringBeforeNth, and SubstringAfterNth, and tests.
1 parent f5b4658 commit 7a905b5

File tree

3 files changed

+261
-13
lines changed

3 files changed

+261
-13
lines changed

OnixLabs.Core.UnitTests/StringExtensionTests.cs

+98
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,38 @@ namespace OnixLabs.Core.UnitTests;
2020

2121
public sealed class StringExtensionTests
2222
{
23+
[Theory(DisplayName = "String.NthIndexOf should return the expected result")]
24+
[InlineData("First:Second:Third:Fourth", ':', 0, -1)]
25+
[InlineData("First:Second:Third:Fourth", ':', 1, 5)]
26+
[InlineData("First:Second:Third:Fourth", ':', 2, 12)]
27+
[InlineData("First:Second:Third:Fourth", ':', 3, 18)]
28+
[InlineData("First:Second:Third:Fourth", ':', 4, -1)]
29+
[InlineData("First:Second:Third:Fourth", ':', 100, -1)]
30+
public void NthIndexOfCharShouldProduceExpectedResult(string value, char seekValue, int count, int expected)
31+
{
32+
// When
33+
int actual = value.NthIndexOf(seekValue, count);
34+
35+
// Then
36+
Assert.Equal(expected, actual);
37+
}
38+
39+
[Theory(DisplayName = "String.NthIndexOf should return the expected result")]
40+
[InlineData("First_split_Second_split_Third_split_Fourth", "_split_", 0, -1)]
41+
[InlineData("First_split_Second_split_Third_split_Fourth", "_split_", 1, 5)]
42+
[InlineData("First_split_Second_split_Third_split_Fourth", "_split_", 2, 18)]
43+
[InlineData("First_split_Second_split_Third_split_Fourth", "_split_", 3, 30)]
44+
[InlineData("First_split_Second_split_Third_split_Fourth", "_split_", 4, -1)]
45+
[InlineData("First_split_Second_split_Third_split_Fourth", "_split_", 100, -1)]
46+
public void NthIndexOfStringShouldProduceExpectedResult(string value, string seekValue, int count, int expected)
47+
{
48+
// When
49+
int actual = value.NthIndexOf(seekValue, count);
50+
51+
// Then
52+
Assert.Equal(expected, actual);
53+
}
54+
2355
[Theory(DisplayName = "String.Repeat should return the expected result")]
2456
[InlineData("0", 10, "0000000000")]
2557
[InlineData("Abc1", 3, "Abc1Abc1Abc1")]
@@ -128,6 +160,72 @@ public void SubstringAfterLastShouldProduceExpectedResultString(string value, st
128160
Assert.Equal(expected, actual);
129161
}
130162

163+
[Theory(DisplayName = "String.SubstringBeforeNth(char) should return the expected substring or default value")]
164+
[InlineData("One:Two:Three:Four", ':', 1, null, "One")]
165+
[InlineData("One:Two:Three:Four", ':', 2, null, "One:Two")]
166+
[InlineData("One:Two:Three:Four", ':', 3, null, "One:Two:Three")]
167+
[InlineData("One:Two:Three:Four", ':', 4, null, "One:Two:Three:Four")]
168+
[InlineData("One:Two:Three:Four", ':', 4, "NOT_FOUND", "NOT_FOUND")]
169+
[InlineData("One:Two:Three", ':', 0, null, "One:Two:Three")]
170+
[InlineData("One:Two:Three", ':', -1, null, "One:Two:Three")]
171+
public void SubstringBeforeNthCharShouldProduceExpectedResult(string value, char delimiter, int nth, string? defaultValue, string expected)
172+
{
173+
// When
174+
string actual = value.SubstringBeforeNth(delimiter, nth, defaultValue);
175+
176+
// Then
177+
Assert.Equal(expected, actual);
178+
}
179+
180+
[Theory(DisplayName = "String.SubstringBeforeNth(string) should return the expected substring or default value")]
181+
[InlineData("One_split_Two_split_Three_split_Four", "_split_", 1, null, "One")]
182+
[InlineData("One_split_Two_split_Three_split_Four", "_split_", 2, null, "One_split_Two")]
183+
[InlineData("One_split_Two_split_Three_split_Four", "_split_", 3, null, "One_split_Two_split_Three")]
184+
[InlineData("One_split_Two_split_Three_split_Four", "_split_", 4, null, "One_split_Two_split_Three_split_Four")]
185+
[InlineData("One_split_Two_split_Three_split_Four", "_split_", 4, "NOT_FOUND", "NOT_FOUND")]
186+
[InlineData("NoDelimiterHere", "_split_", 1, null, "NoDelimiterHere")]
187+
public void SubstringBeforeNthStringShouldProduceExpectedResult(string value, string delimiter, int nth, string? defaultValue, string expected)
188+
{
189+
// When
190+
string actual = value.SubstringBeforeNth(delimiter, nth, defaultValue);
191+
192+
// Then
193+
Assert.Equal(expected, actual);
194+
}
195+
196+
[Theory(DisplayName = "String.SubstringAfterNth(char) should return the expected substring or default value")]
197+
[InlineData("One:Two:Three:Four", ':', 1, null, "Two:Three:Four")]
198+
[InlineData("One:Two:Three:Four", ':', 2, null, "Three:Four")]
199+
[InlineData("One:Two:Three:Four", ':', 3, null, "Four")]
200+
[InlineData("One:Two:Three:Four", ':', 4, null, "One:Two:Three:Four")]
201+
[InlineData("One:Two:Three:Four", ':', 4, "NOT_FOUND", "NOT_FOUND")]
202+
[InlineData("One:Two:Three", ':', 0, null, "One:Two:Three")]
203+
[InlineData("One:Two:Three", ':', -1, null, "One:Two:Three")]
204+
public void SubstringAfterNthCharShouldProduceExpectedResult(string value, char delimiter, int nth, string? defaultValue, string expected)
205+
{
206+
// When
207+
string actual = value.SubstringAfterNth(delimiter, nth, defaultValue);
208+
209+
// Then
210+
Assert.Equal(expected, actual);
211+
}
212+
213+
[Theory(DisplayName = "String.SubstringAfterNth(string) should return the expected substring or default value")]
214+
[InlineData("One_split_Two_split_Three_split_Four", "_split_", 1, null, "Two_split_Three_split_Four")]
215+
[InlineData("One_split_Two_split_Three_split_Four", "_split_", 2, null, "Three_split_Four")]
216+
[InlineData("One_split_Two_split_Three_split_Four", "_split_", 3, null, "Four")]
217+
[InlineData("One_split_Two_split_Three_split_Four", "_split_", 4, null, "One_split_Two_split_Three_split_Four")]
218+
[InlineData("One_split_Two_split_Three_split_Four", "_split_", 4, "NOT_FOUND", "NOT_FOUND")]
219+
[InlineData("NoDelimiterHere", "_split_", 1, null, "NoDelimiterHere")]
220+
public void SubstringAfterNthStringShouldProduceExpectedResult(string value, string delimiter, int nth, string? defaultValue, string expected)
221+
{
222+
// When
223+
string actual = value.SubstringAfterNth(delimiter, nth, defaultValue);
224+
225+
// Then
226+
Assert.Equal(expected, actual);
227+
}
228+
131229
[Theory(DisplayName = "String.ToByteArray should produce the byte array equivalent of the current string")]
132230
[InlineData("Hello, World!", new byte[] { 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21 })]
133231
public void ToByteArrayShouldProduceExpectedResult(string value, byte[] expected)

OnixLabs.Core/Extensions.String.cs

+163-5
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,78 @@ public static class StringExtensions
4242
/// </summary>
4343
private const DateTimeStyles DefaultStyles = DateTimeStyles.None;
4444

45+
/// <summary>
46+
/// Obtains the zero-based index of the nth occurrence of the specified character in this instance.
47+
/// If the specified occurrence does not exist, returns -1.
48+
/// </summary>
49+
/// <param name="value">The string to search.</param>
50+
/// <param name="seekValue">The character to seek.</param>
51+
/// <param name="count">The number of occurrences to skip before returning an index.</param>
52+
/// <returns>
53+
/// Returns the zero-based index position of the nth occurrence of <paramref name="seekValue"/>, if found; otherwise, -1.
54+
/// </returns>
55+
public static int NthIndexOf(this string value, char seekValue, int count)
56+
{
57+
if (string.IsNullOrEmpty(value) || count <= 0) return -1;
58+
59+
int occurrences = 0;
60+
61+
for (int i = 0; i < value.Length; i++)
62+
{
63+
if (value[i] != seekValue) continue;
64+
65+
occurrences++;
66+
67+
if (occurrences != count) continue;
68+
69+
return i;
70+
}
71+
72+
return NotFound;
73+
}
74+
75+
/// <summary>
76+
/// Obtains the zero-based index of the nth occurrence of the specified character in this instance.
77+
/// If the specified occurrence does not exist, returns -1.
78+
/// </summary>
79+
/// <param name="value">The string to search.</param>
80+
/// <param name="seekValue">The substring to seek.</param>
81+
/// <param name="count">The number of occurrences to skip before returning an index.</param>
82+
/// <param name="comparison">The comparison that will be used to compare the current value and the seek value.</param>
83+
/// <returns>
84+
/// Returns the zero-based index position of the nth occurrence of <paramref name="seekValue"/>, if found; otherwise, -1.
85+
/// </returns>
86+
public static int NthIndexOf(this string value, string seekValue, int count, StringComparison comparison = DefaultComparison)
87+
{
88+
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(seekValue) || count <= 0) return -1;
89+
90+
int occurrences = 0;
91+
int startIndex = 0;
92+
93+
while (true)
94+
{
95+
int index = value.IndexOf(seekValue, startIndex, comparison);
96+
97+
if (index == -1) return -1;
98+
99+
occurrences++;
100+
101+
if (occurrences == count) return index;
102+
103+
startIndex = index + seekValue.Length;
104+
105+
if (startIndex >= value.Length) return NotFound;
106+
}
107+
}
108+
45109
/// <summary>
46110
/// Repeats the current <see cref="String"/> by the specified number of repetitions.
47111
/// </summary>
48112
/// <param name="value">The <see cref="String"/> instance to repeat.</param>
49113
/// <param name="count">The number of repetitions of the current <see cref="String"/> instance.</param>
50114
/// <returns>Returns a new <see cref="String"/> instance representing the repetition of the current <see cref="String"/> instance.</returns>
51-
public static string Repeat(this string value, int count) => count > 0 ? string.Join(string.Empty, Enumerable.Repeat(value, count)) : string.Empty;
115+
public static string Repeat(this string value, int count) =>
116+
count > 0 ? string.Join(string.Empty, Enumerable.Repeat(value, count)) : string.Empty;
52117

53118
/// <summary>
54119
/// Obtains a sub-string before the specified index within the current <see cref="String"/> instance.
@@ -64,7 +129,8 @@ public static class StringExtensions
64129
/// If the default value is <see langword="null"/>, then the current <see cref="String"/> instance will be returned.
65130
/// </returns>
66131
// ReSharper disable once HeapView.ObjectAllocation
67-
private static string SubstringBeforeIndex(this string value, int index, string? defaultValue = null) => index <= NotFound ? defaultValue ?? value : value[..index];
132+
private static string SubstringBeforeIndex(this string value, int index, string? defaultValue = null) =>
133+
index <= NotFound ? defaultValue ?? value : value[..index];
68134

69135
/// <summary>
70136
/// Obtains a sub-string after the specified index within the current <see cref="String"/> instance.
@@ -81,7 +147,8 @@ public static class StringExtensions
81147
/// If the default value is <see langword="null"/>, then the current <see cref="String"/> instance will be returned.
82148
/// </returns>
83149
// ReSharper disable once HeapView.ObjectAllocation
84-
private static string SubstringAfterIndex(this string value, int index, int offset, string? defaultValue = null) => index <= NotFound ? defaultValue ?? value : value[(index + offset)..value.Length];
150+
private static string SubstringAfterIndex(this string value, int index, int offset, string? defaultValue = null) =>
151+
index <= NotFound ? defaultValue ?? value : value[(index + offset)..value.Length];
85152

86153
/// <summary>
87154
/// Obtains a sub-string before the first occurrence of the specified delimiter within the current <see cref="String"/> instance.
@@ -235,6 +302,95 @@ public static string SubstringAfterLast(this string value, char delimiter, strin
235302
public static string SubstringAfterLast(this string value, string delimiter, string? defaultValue = null, StringComparison comparison = DefaultComparison) =>
236303
value.SubstringAfterIndex(value.LastIndexOf(delimiter, comparison), 1, defaultValue);
237304

305+
/// <summary>
306+
/// Obtains a sub-string before the nth occurrence of the specified character within the current <see cref="String"/> instance.
307+
/// If the nth occurrence is not found, returns the <paramref name="defaultValue"/> or the entire string if default is null.
308+
/// </summary>
309+
/// <param name="value">The current <see cref="String"/> instance from which to obtain a sub-string.</param>
310+
/// <param name="seekValue">The character to find the nth occurrence of.</param>
311+
/// <param name="count">The nth occurrence to find.</param>
312+
/// <param name="defaultValue">
313+
/// The <see cref="String"/> value to return if the nth occurrence is not found.
314+
/// If the default value is <see langword="null"/>, the current <see cref="String"/> instance is returned.
315+
/// </param>
316+
/// <returns>
317+
/// A sub-string before the nth occurrence of <paramref name="seekValue"/> if found; otherwise,
318+
/// <paramref name="defaultValue"/> or the entire string if default is null.
319+
/// </returns>
320+
public static string SubstringBeforeNth(this string value, char seekValue, int count, string? defaultValue = null)
321+
{
322+
int index = value.NthIndexOf(seekValue, count);
323+
return value.SubstringBeforeIndex(index, defaultValue);
324+
}
325+
326+
/// <summary>
327+
/// Obtains a sub-string before the nth occurrence of the specified substring within the current <see cref="String"/> instance.
328+
/// If the nth occurrence is not found, returns the <paramref name="defaultValue"/> or the entire string if default is null.
329+
/// </summary>
330+
/// <param name="value">The current <see cref="String"/> instance from which to obtain a sub-string.</param>
331+
/// <param name="seekValue">The substring to find the nth occurrence of.</param>
332+
/// <param name="count">The nth occurrence to find.</param>
333+
/// <param name="defaultValue">
334+
/// The <see cref="String"/> value to return if the nth occurrence is not found.
335+
/// If the default value is <see langword="null"/>, the current <see cref="String"/> instance is returned.
336+
/// </param>
337+
/// <param name="comparison">The comparison that will be used to compare the current value and the seek value.</param>
338+
/// <returns>
339+
/// A sub-string before the nth occurrence of <paramref name="seekValue"/> if found; otherwise,
340+
/// <paramref name="defaultValue"/> or the entire string if default is null.
341+
/// </returns>
342+
public static string SubstringBeforeNth(this string value, string seekValue, int count, string? defaultValue = null, StringComparison comparison = DefaultComparison)
343+
{
344+
int index = value.NthIndexOf(seekValue, count, comparison);
345+
return value.SubstringBeforeIndex(index, defaultValue);
346+
}
347+
348+
/// <summary>
349+
/// Obtains a sub-string after the nth occurrence of the specified character within the current <see cref="String"/> instance.
350+
/// If the nth occurrence is not found, returns the <paramref name="defaultValue"/> or the entire string if default is null.
351+
/// </summary>
352+
/// <param name="value">The current <see cref="String"/> instance from which to obtain a sub-string.</param>
353+
/// <param name="seekValue">The character to find the nth occurrence of.</param>
354+
/// <param name="count">The nth occurrence to find.</param>
355+
/// <param name="defaultValue">
356+
/// The <see cref="String"/> value to return if the nth occurrence is not found.
357+
/// If the default value is <see langword="null"/>, the current <see cref="String"/> instance is returned.
358+
/// </param>
359+
/// <returns>
360+
/// A sub-string after the nth occurrence of <paramref name="seekValue"/> if found; otherwise,
361+
/// <paramref name="defaultValue"/> or the entire string if default is null.
362+
/// </returns>
363+
public static string SubstringAfterNth(this string value, char seekValue, int count, string? defaultValue = null)
364+
{
365+
int index = value.NthIndexOf(seekValue, count);
366+
// Move 1 character after the nth occurrence index.
367+
return value.SubstringAfterIndex(index, 1, defaultValue);
368+
}
369+
370+
/// <summary>
371+
/// Obtains a sub-string after the nth occurrence of the specified substring within the current <see cref="String"/> instance.
372+
/// If the nth occurrence is not found, returns the <paramref name="defaultValue"/> or the entire string if default is null.
373+
/// </summary>
374+
/// <param name="value">The current <see cref="String"/> instance from which to obtain a sub-string.</param>
375+
/// <param name="seekValue">The substring to find the nth occurrence of.</param>
376+
/// <param name="count">The nth occurrence to find.</param>
377+
/// <param name="defaultValue">
378+
/// The <see cref="String"/> value to return if the nth occurrence is not found.
379+
/// If the default value is <see langword="null"/>, the current <see cref="String"/> instance is returned.
380+
/// </param>
381+
/// <param name="comparison">The comparison that will be used to compare the current value and the seek value.</param>
382+
/// <returns>
383+
/// A sub-string after the nth occurrence of <paramref name="seekValue"/> if found; otherwise,
384+
/// <paramref name="defaultValue"/> or the entire string if default is null.
385+
/// </returns>
386+
public static string SubstringAfterNth(this string value, string seekValue, int count, string? defaultValue = null, StringComparison comparison = DefaultComparison)
387+
{
388+
int index = value.NthIndexOf(seekValue, count, comparison);
389+
// Move by the length of the found substring after the nth occurrence index.
390+
int offset = (index != NotFound && !string.IsNullOrEmpty(seekValue)) ? seekValue.Length : 0;
391+
return value.SubstringAfterIndex(index, offset, defaultValue);
392+
}
393+
238394
/// <summary>
239395
/// Converts the current <see cref="String"/> instance into a new <see cref="T:Byte[]"/> instance.
240396
/// </summary>
@@ -336,7 +492,8 @@ public static bool TryCopyTo(this string value, Span<char> destination, out int
336492
/// <param name="before">The <see cref="Char"/> that should precede the current <see cref="String"/> instance.</param>
337493
/// <param name="after">The <see cref="Char"/> that should succeed the current <see cref="String"/> instance.</param>
338494
/// <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>
339-
public static string Wrap(this string value, char before, char after) => string.Concat(before.ToString(), value, after.ToString());
495+
public static string Wrap(this string value, char before, char after) =>
496+
string.Concat(before.ToString(), value, after.ToString());
340497

341498
/// <summary>
342499
/// Wraps the current <see cref="String"/> instance between the specified before and after <see cref="String"/> instances.
@@ -345,5 +502,6 @@ public static bool TryCopyTo(this string value, Span<char> destination, out int
345502
/// <param name="before">The <see cref="String"/> that should precede the current <see cref="String"/> instance.</param>
346503
/// <param name="after">The <see cref="String"/> that should succeed the current <see cref="String"/> instance.</param>
347504
/// <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>
348-
public static string Wrap(this string value, string before, string after) => string.Concat(before, value, after);
505+
public static string Wrap(this string value, string before, string after) =>
506+
string.Concat(before, value, after);
349507
}

OnixLabs.Playground/Program.cs

-8
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,11 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
using System;
16-
using OnixLabs.Security.Cryptography;
17-
1815
namespace OnixLabs.Playground;
1916

2017
internal static class Program
2118
{
2219
private static void Main()
2320
{
24-
string value = "SHA256:043a718774c572bd8a25adbeb1bfcd5c0256ae11cecf9f9c3f925d0e52beaf89";
25-
26-
NamedHash hash = NamedHash.Parse(value);
27-
28-
Console.WriteLine(hash);
2921
}
3022
}

0 commit comments

Comments
 (0)