From c3aab84419bf4a5a9d2c871dcb3973db062b4c53 Mon Sep 17 00:00:00 2001 From: sdwck <121903864+sdwck@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:14:48 +0300 Subject: [PATCH 1/3] fix: anchor Email regex to prevent substring matching & perf: optimize RemoveCharacters via SearchValues --- Benchmarks/Benchmark.cs | 37 +++++++++-- StringExtension/StringExtension.cs | 80 +++++++++++++++++++++++- StringExtension/Validation/Validation.cs | 2 +- UnitTests/StringExtensionTests.cs | 42 +++++++++++++ 4 files changed, 155 insertions(+), 6 deletions(-) diff --git a/Benchmarks/Benchmark.cs b/Benchmarks/Benchmark.cs index c0ac8b1..e18a3c1 100644 --- a/Benchmarks/Benchmark.cs +++ b/Benchmarks/Benchmark.cs @@ -1,4 +1,4 @@ -using Benchmark; +using System.Buffers; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; @@ -7,7 +7,7 @@ using StringExtension.Linguistics; using StringExtension.Validation; -BenchmarkRunner.Run( +BenchmarkSwitcher.FromAssembly(typeof(Benchmark.StringExtensionBenchmark).Assembly).Run(args, ManualConfig.Create(DefaultConfig.Instance.WithOptions(ConfigOptions.DisableOptimizationsValidator))); namespace Benchmark @@ -19,20 +19,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 4421662..22c49e7 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 814f603..089c459 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 dc53768..3307c5c 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("he wrd!")); + } + /// /// 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. /// From 07fa87bea87d4cac332f4212a5bd811a739a4a72 Mon Sep 17 00:00:00 2001 From: sdwck <121903864+sdwck@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:34:05 +0300 Subject: [PATCH 2/3] fix: nullable annotations & separate PR for benchmark switcher --- Benchmarks/Benchmark.cs | 3 ++- StringExtension/StringExtension.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Benchmarks/Benchmark.cs b/Benchmarks/Benchmark.cs index e18a3c1..65033ce 100644 --- a/Benchmarks/Benchmark.cs +++ b/Benchmarks/Benchmark.cs @@ -1,4 +1,5 @@ using System.Buffers; +using Benchmark; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; @@ -7,7 +8,7 @@ using StringExtension.Linguistics; using StringExtension.Validation; -BenchmarkSwitcher.FromAssembly(typeof(Benchmark.StringExtensionBenchmark).Assembly).Run(args, +BenchmarkRunner.Run( ManualConfig.Create(DefaultConfig.Instance.WithOptions(ConfigOptions.DisableOptimizationsValidator))); namespace Benchmark diff --git a/StringExtension/StringExtension.cs b/StringExtension/StringExtension.cs index 22c49e7..27138b2 100644 --- a/StringExtension/StringExtension.cs +++ b/StringExtension/StringExtension.cs @@ -72,7 +72,7 @@ public static string RemoveCharacters(this ReadOnlySpan input, ReadOnlySpa /// 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) + public static string? RemoveCharacters(this string? input, SearchValues? searchValues) { if (string.IsNullOrEmpty(input) || searchValues is null) { @@ -88,7 +88,7 @@ public static string RemoveCharacters(this string input, SearchValues sear /// 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) + public static string RemoveCharacters(this ReadOnlySpan input, SearchValues? searchValues) { if (input.IsEmpty || searchValues is null) { From 1288b5ae4b8b53b9bfde89b3ac7581724fe968b5 Mon Sep 17 00:00:00 2001 From: Antyss77 Date: Tue, 11 Aug 2026 21:21:42 +0200 Subject: [PATCH 3/3] test: use the expected variable instead of a duplicated literal --- UnitTests/StringExtensionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnitTests/StringExtensionTests.cs b/UnitTests/StringExtensionTests.cs index ecf4d87..4532ca4 100644 --- a/UnitTests/StringExtensionTests.cs +++ b/UnitTests/StringExtensionTests.cs @@ -47,7 +47,7 @@ public void TestRemoveCharacters_SearchValues_Span() var searchValues = SearchValues.Create('l', 'o'); var expected = "he wrd!"; var result = input.RemoveCharacters(searchValues); - Assert.That(result, Is.EqualTo("he wrd!")); +Assert.That(result, Is.EqualTo(expected)); } ///