diff --git a/Benchmarks/Benchmark.cs b/Benchmarks/Benchmark.cs index 7f42590..93df60f 100644 --- a/Benchmarks/Benchmark.cs +++ b/Benchmarks/Benchmark.cs @@ -1,4 +1,5 @@ -using Benchmark; +using System.Buffers; +using Benchmark; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; @@ -19,20 +20,49 @@ namespace Benchmark public class StringExtensionBenchmark { private readonly string input = "hello world!"; + private readonly string longInput = string.Concat(Enumerable.Repeat("hello world! ", 100)); private readonly char[] charactersToRemove = { 'l', 'o' }; + private readonly SearchValues searchValues = SearchValues.Create('l', 'o'); private readonly string substring = "l"; private readonly string phoneNumber = "555-555-5555"; private readonly string email = "john.doe@example.com"; /// - /// Benchmark for the RemoveCharacters method. + /// Benchmark for the RemoveCharacters method with char[]. /// [Benchmark] - public string? RemoveCharacters() + public string? RemoveCharacters_CharArray() { return input.RemoveCharacters(charactersToRemove); } + /// + /// Benchmark for the RemoveCharacters method with SearchValues. + /// + [Benchmark] + public string? RemoveCharacters_SearchValues() + { + return input.RemoveCharacters(searchValues); + } + + /// + /// Benchmark for the RemoveCharacters method on long input with char[]. + /// + [Benchmark] + public string? RemoveCharacters_Long_CharArray() + { + return longInput.RemoveCharacters(charactersToRemove); + } + + /// + /// Benchmark for the RemoveCharacters method on long input with SearchValues. + /// + [Benchmark] + public string? RemoveCharacters_Long_SearchValues() + { + return longInput.RemoveCharacters(searchValues); + } + /// /// Benchmark for the IsValidEmail method. /// diff --git a/StringExtension/StringExtension.cs b/StringExtension/StringExtension.cs index 880ceee..dbc85d5 100644 --- a/StringExtension/StringExtension.cs +++ b/StringExtension/StringExtension.cs @@ -1,4 +1,4 @@ -using System.Buffers; +using System.Buffers; using StringExtension.Internal; namespace StringExtension; @@ -65,6 +65,84 @@ public static string RemoveCharacters(this ReadOnlySpan input, ReadOnlySpa } } + /// + /// Removes specified characters from the given string using a set. + /// + /// The input string. + /// The set of characters to remove. + /// A new string with specified characters removed. + /// Returns if is . + public static string? RemoveCharacters(this string? input, SearchValues? searchValues) + { + if (string.IsNullOrEmpty(input) || searchValues is null) + { + return input; + } + + return input.AsSpan().RemoveCharacters(searchValues); + } + + /// + /// Removes specified characters from the given span of characters using a set. + /// + /// The input characters. + /// The set of characters to remove. + /// A new string with specified characters removed. + public static string RemoveCharacters(this ReadOnlySpan input, SearchValues? searchValues) + { + if (input.IsEmpty || searchValues is null) + { + return input.ToString(); + } + + var firstIndex = input.IndexOfAny(searchValues); + if (firstIndex < 0) + { + return input.ToString(); + } + + char[]? pooledBuffer = null; + var buffer = (uint)input.Length <= BufferLimits.StackAllocThreshold + ? stackalloc char[input.Length] + : pooledBuffer = ArrayPool.Shared.Rent(input.Length); + + try + { + input[..firstIndex].CopyTo(buffer); + var destinationIndex = firstIndex; + var remainder = input[firstIndex..]; + + while (true) + { + remainder = remainder[1..]; + + var nextMatch = remainder.IndexOfAny(searchValues); + if (nextMatch < 0) + { + remainder.CopyTo(buffer[destinationIndex..]); + destinationIndex += remainder.Length; + break; + } + + if (nextMatch > 0) + { + remainder[..nextMatch].CopyTo(buffer[destinationIndex..]); + destinationIndex += nextMatch; + remainder = remainder[nextMatch..]; + } + } + + return new string(buffer[..destinationIndex]); + } + finally + { + if (pooledBuffer is not null) + { + ArrayPool.Shared.Return(pooledBuffer); + } + } + } + /// /// Counts the number of occurrences of a substring in the given string. /// diff --git a/StringExtension/Validation/Validation.cs b/StringExtension/Validation/Validation.cs index acbb74d..65cd480 100644 --- a/StringExtension/Validation/Validation.cs +++ b/StringExtension/Validation/Validation.cs @@ -11,7 +11,7 @@ public static partial class Validation /// Represents a regular expression that can be used to validate an email address. /// /// A regular expression that can be used to validate an email address. - [GeneratedRegex(@"[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+")] + [GeneratedRegex(@"^[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+$")] private static partial Regex MailAddressRegex(); /// diff --git a/UnitTests/StringExtensionTests.cs b/UnitTests/StringExtensionTests.cs index a9271a3..4532ca4 100644 --- a/UnitTests/StringExtensionTests.cs +++ b/UnitTests/StringExtensionTests.cs @@ -1,3 +1,4 @@ +using System.Buffers; using StringExtension; using StringExtension.Casing; using StringExtension.Linguistics; @@ -23,6 +24,32 @@ public void TestRemoveCharacters() Assert.That(result, Is.EqualTo(expected)); } + /// + /// Tests the RemoveCharacters method with SearchValues. + /// + [Test] + public void TestRemoveCharacters_SearchValues() + { + var input = "hello world!"; + var searchValues = SearchValues.Create('l', 'o'); + var expected = "he wrd!"; + var result = input.RemoveCharacters(searchValues); + Assert.That(result, Is.EqualTo(expected)); + } + + /// + /// Tests the ReadOnlySpan overload of RemoveCharacters with SearchValues. + /// + [Test] + public void TestRemoveCharacters_SearchValues_Span() + { + ReadOnlySpan input = "hello world!"; + var searchValues = SearchValues.Create('l', 'o'); + var expected = "he wrd!"; + var result = input.RemoveCharacters(searchValues); +Assert.That(result, Is.EqualTo(expected)); + } + /// /// Tests that RemoveCharacters handles a null input gracefully. /// @@ -59,6 +86,21 @@ public void TestIsValidEmail() Assert.That(result, Is.True); } + /// + /// Tests that IsValidEmail rejects strings that only contain an email as a substring. + /// + [Test] + public void TestIsValidEmail_InvalidSubstrings() + { + string emailWithPrefix = "hello john.doe@example.com"; + string emailWithSuffix = "john.doe@example.com world"; + string emailInSentence = "contact john.doe@example.com for info"; + + Assert.That(emailWithPrefix.IsValidEmail(), Is.False); + Assert.That(emailWithSuffix.IsValidEmail(), Is.False); + Assert.That(emailInSentence.IsValidEmail(), Is.False); + } + /// /// Tests the IsValidPhoneNumber method. ///