Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions Benchmarks/Benchmark.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Benchmark;
using System.Buffers;
using Benchmark;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
Expand All @@ -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<char> 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";

/// <summary>
/// Benchmark for the RemoveCharacters method.
/// Benchmark for the RemoveCharacters method with char[].
/// </summary>
[Benchmark]
public string? RemoveCharacters()
public string? RemoveCharacters_CharArray()
{
return input.RemoveCharacters(charactersToRemove);
}

/// <summary>
/// Benchmark for the RemoveCharacters method with SearchValues.
/// </summary>
[Benchmark]
public string? RemoveCharacters_SearchValues()
{
return input.RemoveCharacters(searchValues);
}

/// <summary>
/// Benchmark for the RemoveCharacters method on long input with char[].
/// </summary>
[Benchmark]
public string? RemoveCharacters_Long_CharArray()
{
return longInput.RemoveCharacters(charactersToRemove);
}

/// <summary>
/// Benchmark for the RemoveCharacters method on long input with SearchValues.
/// </summary>
[Benchmark]
public string? RemoveCharacters_Long_SearchValues()
{
return longInput.RemoveCharacters(searchValues);
}

/// <summary>
/// Benchmark for the IsValidEmail method.
/// </summary>
Expand Down
80 changes: 79 additions & 1 deletion StringExtension/StringExtension.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Buffers;
using System.Buffers;
using StringExtension.Internal;

namespace StringExtension;
Expand Down Expand Up @@ -65,6 +65,84 @@ public static string RemoveCharacters(this ReadOnlySpan<char> input, ReadOnlySpa
}
}

/// <summary>
/// Removes specified characters from the given string using a <see cref="SearchValues{T}"/> set.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="searchValues">The set of characters to remove.</param>
/// <returns>A new string with specified characters removed.</returns>
/// <remarks>Returns <see langword="null"/> if <paramref name="input"/> is <see langword="null"/>.</remarks>
public static string? RemoveCharacters(this string? input, SearchValues<char>? searchValues)
{
if (string.IsNullOrEmpty(input) || searchValues is null)
{
return input;
}

return input.AsSpan().RemoveCharacters(searchValues);
}

/// <summary>
/// Removes specified characters from the given span of characters using a <see cref="SearchValues{T}"/> set.
/// </summary>
/// <param name="input">The input characters.</param>
/// <param name="searchValues">The set of characters to remove.</param>
/// <returns>A new string with specified characters removed.</returns>
public static string RemoveCharacters(this ReadOnlySpan<char> input, SearchValues<char>? 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<char>.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<char>.Shared.Return(pooledBuffer);
}
}
}

/// <summary>
/// Counts the number of occurrences of a substring in the given string.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion StringExtension/Validation/Validation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static partial class Validation
/// Represents a regular expression that can be used to validate an email address.
/// </summary>
/// <returns>A regular expression that can be used to validate an email address.</returns>
[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();

/// <summary>
Expand Down
42 changes: 42 additions & 0 deletions UnitTests/StringExtensionTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Buffers;
using StringExtension;
using StringExtension.Casing;
using StringExtension.Linguistics;
Expand All @@ -23,6 +24,32 @@ public void TestRemoveCharacters()
Assert.That(result, Is.EqualTo(expected));
}

/// <summary>
/// Tests the RemoveCharacters method with SearchValues.
/// </summary>
[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));
}

/// <summary>
/// Tests the ReadOnlySpan overload of RemoveCharacters with SearchValues.
/// </summary>
[Test]
public void TestRemoveCharacters_SearchValues_Span()
{
ReadOnlySpan<char> input = "hello world!";
var searchValues = SearchValues.Create('l', 'o');
var expected = "he wrd!";
var result = input.RemoveCharacters(searchValues);
Assert.That(result, Is.EqualTo(expected));
}

/// <summary>
/// Tests that RemoveCharacters handles a null input gracefully.
/// </summary>
Expand Down Expand Up @@ -59,6 +86,21 @@ public void TestIsValidEmail()
Assert.That(result, Is.True);
}

/// <summary>
/// Tests that IsValidEmail rejects strings that only contain an email as a substring.
/// </summary>
[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);
}

/// <summary>
/// Tests the IsValidPhoneNumber method.
/// </summary>
Expand Down
Loading