From f5b4658321f32109a0b9f1a3e3a5ff22849797b9 Mon Sep 17 00:00:00 2001 From: Matthew Layton <9935122+MrMatthewLayton@users.noreply.github.com> Date: Sat, 11 Jan 2025 16:44:22 +0000 Subject: [PATCH 1/2] Added Length property to Hash and Salt structs. (#92) Added Length property to Hash and Salt structs. --- Directory.Build.props | 6 ++-- .../HashTests.cs | 28 +++++++++++++++++++ .../SaltTests.cs | 22 +++++++++++++++ OnixLabs.Security.Cryptography/Hash.cs | 7 ++++- OnixLabs.Security.Cryptography/MerkleTree.cs | 3 +- OnixLabs.Security.Cryptography/Salt.cs | 7 ++++- 6 files changed, 66 insertions(+), 7 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 4e9bc56..23700e2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,8 +8,8 @@ en Copyright © ONIXLabs 2020 https://github.com/onix-labs/onixlabs-dotnet - 11.1.0 - 11.1.0 - 11.1.0 + 11.2.0 + 11.2.0 + 11.2.0 diff --git a/OnixLabs.Security.Cryptography.UnitTests/HashTests.cs b/OnixLabs.Security.Cryptography.UnitTests/HashTests.cs index 4024f98..34b2408 100644 --- a/OnixLabs.Security.Cryptography.UnitTests/HashTests.cs +++ b/OnixLabs.Security.Cryptography.UnitTests/HashTests.cs @@ -36,6 +36,7 @@ public void HashShouldBeConstructableFromBytes() // Then Assert.Equal(expected, actual); + Assert.Equal(16, candidate.Length); } [Fact(DisplayName = "Hash should be constructable from byte and length")] @@ -52,6 +53,7 @@ public void HashShouldBeConstructableFromByteAndLength() // Then Assert.Equal(expected, actual); + Assert.Equal(16, candidate.Length); } [Fact(DisplayName = "Hash value should not be modified when altering the original byte array")] @@ -68,6 +70,7 @@ public void HashValueShouldNotBeModifiedWhenAlteringTheOriginalByteArray() // Then Assert.Equal(expected, actual); + Assert.Equal(4, candidate.Length); } [Fact(DisplayName = "Hash value should not be modified when altering the obtained byte array")] @@ -83,6 +86,7 @@ public void HashValueShouldNotBeModifiedWhenAlteringTheObtainedByteArray() // Then Assert.Equal(expected, actual); + Assert.Equal(4, candidate.Length); } [Fact(DisplayName = "Identical default hash values should be considered equal")] @@ -98,6 +102,9 @@ public void IdenticalDefaultHashValuesShouldBeConsideredEqual() Assert.True(left.Equals(right)); Assert.True(left == right); Assert.False(left != right); + + Assert.Equal(0, left.Length); + Assert.Equal(0, right.Length); } [Fact(DisplayName = "Identical hash values should be considered equal")] @@ -113,6 +120,9 @@ public void IdenticalHashValuesShouldBeConsideredEqual() Assert.True(left.Equals(right)); Assert.True(left == right); Assert.False(left != right); + + Assert.Equal(4, left.Length); + Assert.Equal(4, right.Length); } [Fact(DisplayName = "Different hash values should not be considered equal")] @@ -128,6 +138,9 @@ public void DifferentHashValuesShouldNotBeConsideredEqual() Assert.False(left.Equals(right)); Assert.False(left == right); Assert.True(left != right); + + Assert.Equal(4, left.Length); + Assert.Equal(4, right.Length); } [Fact(DisplayName = "Identical hash values should produce identical hash codes")] @@ -143,6 +156,9 @@ public void IdenticalHashValuesShouldProduceIdenticalHashCodes() // Then Assert.Equal(leftHashCode, rightHashCode); + + Assert.Equal(4, left.Length); + Assert.Equal(4, right.Length); } [Fact(DisplayName = "Different hash values should produce different hash codes")] @@ -158,6 +174,9 @@ public void DifferentHashValuesShouldProduceDifferentHashCodes() // Then Assert.NotEqual(leftHashCode, rightHashCode); + + Assert.Equal(4, left.Length); + Assert.Equal(4, right.Length); } [Fact(DisplayName = "Hashes should produce a negative-one sort order when the left-hand hash is lesser than the right-hand hash")] @@ -173,6 +192,9 @@ public void HashesShouldProduceANegativeOneSortOrderWhenTheLeftHandHashIsLesserT // Then Assert.Equal(expected, actual); + + Assert.Equal(1, left.Length); + Assert.Equal(1, right.Length); } [Fact(DisplayName = "Hashes should produce a positive-one sort order when the left-hand hash is greater than the right-hand hash")] @@ -188,6 +210,9 @@ public void HashesShouldProduceAPositiveOneSortOrderWhenTheLeftHandHashIsGreater // Then Assert.Equal(expected, actual); + + Assert.Equal(1, left.Length); + Assert.Equal(1, right.Length); } [Fact(DisplayName = "Hashes should produce a zero sort order when the left-hand hash is equal to the right-hand hash")] @@ -203,6 +228,9 @@ public void HashesShouldProduceAZeroSortOrderWhenTheLeftHandHashIsEqualToTheRigh // Then Assert.Equal(expected, actual); + + Assert.Equal(1, left.Length); + Assert.Equal(1, right.Length); } [Theory(DisplayName = "Hash.Compute should produce the expected hash using a byte array")] diff --git a/OnixLabs.Security.Cryptography.UnitTests/SaltTests.cs b/OnixLabs.Security.Cryptography.UnitTests/SaltTests.cs index 49de23c..6274431 100644 --- a/OnixLabs.Security.Cryptography.UnitTests/SaltTests.cs +++ b/OnixLabs.Security.Cryptography.UnitTests/SaltTests.cs @@ -32,6 +32,7 @@ public void SaltShouldBeConstructableFromBytes() // Then Assert.Equal(expected, actual); + Assert.Equal(16, candidate.Length); } [Fact(DisplayName = "Salt value should not be modified when altering the original byte array")] @@ -48,6 +49,7 @@ public void SaltValueShouldNotBeModifiedWhenAlteringTheOriginalByteArray() // Then Assert.Equal(expected, actual); + Assert.Equal(4, candidate.Length); } [Fact(DisplayName = "Salt value should not be modified when altering the obtained byte array")] @@ -63,6 +65,7 @@ public void SaltValueShouldNotBeModifiedWhenAlteringTheObtainedByteArray() // Then Assert.Equal(expected, actual); + Assert.Equal(4, candidate.Length); } [Fact(DisplayName = "Identical default salt values should be considered equal")] @@ -78,6 +81,9 @@ public void IdenticalDefaultSaltValuesShouldBeConsideredEqual() Assert.True(left.Equals(right)); Assert.True(left == right); Assert.False(left != right); + + Assert.Equal(0, left.Length); + Assert.Equal(0, right.Length); } [Fact(DisplayName = "Identical salt values should be considered equal")] @@ -93,6 +99,9 @@ public void IdenticalSaltValuesShouldBeConsideredEqual() Assert.True(left.Equals(right)); Assert.True(left == right); Assert.False(left != right); + + Assert.Equal(4, left.Length); + Assert.Equal(4, right.Length); } [Fact(DisplayName = "Different salt values should not be considered equal")] @@ -108,6 +117,9 @@ public void DifferentSaltValuesShouldNotBeConsideredEqual() Assert.False(left.Equals(right)); Assert.False(left == right); Assert.True(left != right); + + Assert.Equal(4, left.Length); + Assert.Equal(4, right.Length); } [Fact(DisplayName = "Identical salt values should produce identical hash codes")] @@ -123,6 +135,9 @@ public void IdenticalSaltValuesShouldProduceIdenticalSaltCodes() // Then Assert.Equal(leftHashCode, rightHashCode); + + Assert.Equal(4, left.Length); + Assert.Equal(4, right.Length); } [Fact(DisplayName = "Different salt values should produce different hash codes")] @@ -138,6 +153,9 @@ public void DifferentSaltValuesShouldProduceDifferentSaltCodes() // Then Assert.NotEqual(leftHashCode, rightHashCode); + + Assert.Equal(4, left.Length); + Assert.Equal(4, right.Length); } [Fact(DisplayName = "Salt.Create should produce a salt of the specified length")] @@ -149,6 +167,8 @@ public void SaltCreateShouldProduceASaltOfTheSpecifiedLength() // Then Assert.Equal(expected, candidate.AsReadOnlySpan().Length); + + Assert.Equal(32, candidate.Length); } [Fact(DisplayName = "Salt.CreateNonZero should produce a salt of the specified length of non-zero bytes")] @@ -161,5 +181,7 @@ public void SaltCreateNonZeroShouldProduceASaltOfTheSpecifiedLengthOfNonZeroByte // Then Assert.Equal(expected, candidate.AsReadOnlySpan().Length); Assert.True(candidate.AsReadOnlySpan().ToArray().None(value => value is 0)); + + Assert.Equal(32, candidate.Length); } } diff --git a/OnixLabs.Security.Cryptography/Hash.cs b/OnixLabs.Security.Cryptography/Hash.cs index dbb38b1..171e342 100644 --- a/OnixLabs.Security.Cryptography/Hash.cs +++ b/OnixLabs.Security.Cryptography/Hash.cs @@ -23,7 +23,7 @@ namespace OnixLabs.Security.Cryptography; /// Represents a cryptographic hash. /// /// The underlying value of the cryptographic hash. -public readonly partial struct Hash(ReadOnlySpan value) : ICryptoPrimitive, IValueComparable, ISpanParsable, ISpanBinaryConvertible +public readonly partial struct Hash(ReadOnlySpan value) : ICryptoPrimitive, IValueComparable, ISpanParsable { /// /// Initializes a new instance of the struct. @@ -42,4 +42,9 @@ public Hash(byte value, int length) : this(Enumerable.Repeat(value, length).ToAr } private readonly byte[] value = value.ToArray(); + + /// + /// Gets the length of the current in bytes. + /// + public int Length => value?.Length ?? 0; } diff --git a/OnixLabs.Security.Cryptography/MerkleTree.cs b/OnixLabs.Security.Cryptography/MerkleTree.cs index 56b561b..7da96eb 100644 --- a/OnixLabs.Security.Cryptography/MerkleTree.cs +++ b/OnixLabs.Security.Cryptography/MerkleTree.cs @@ -13,14 +13,13 @@ // limitations under the License. using System.Security.Cryptography; -using OnixLabs.Core; namespace OnixLabs.Security.Cryptography; /// /// Represents a Merkle tree. /// -public abstract partial class MerkleTree : ICryptoPrimitive, ISpanBinaryConvertible +public abstract partial class MerkleTree : ICryptoPrimitive { /// /// Initializes a new instance of the class. diff --git a/OnixLabs.Security.Cryptography/Salt.cs b/OnixLabs.Security.Cryptography/Salt.cs index 1ca47db..e86db09 100644 --- a/OnixLabs.Security.Cryptography/Salt.cs +++ b/OnixLabs.Security.Cryptography/Salt.cs @@ -22,7 +22,7 @@ namespace OnixLabs.Security.Cryptography; /// Represents a cryptographically secure random number, otherwise known as a salt value. /// /// The underlying value of the salt. -public readonly partial struct Salt(ReadOnlySpan value) : ICryptoPrimitive, ISpanBinaryConvertible +public readonly partial struct Salt(ReadOnlySpan value) : ICryptoPrimitive { /// /// Initializes a new instance of the struct. @@ -32,4 +32,9 @@ public readonly partial struct Salt(ReadOnlySpan value) : ICryptoPrimitive public Salt(ReadOnlySequence value) : this(ReadOnlySpan.Empty) => value.CopyTo(out this.value); private readonly byte[] value = value.ToArray(); + + /// + /// Gets the length of the current in bytes. + /// + public int Length => value?.Length ?? 0; } 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 2/2] 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); } }