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
3 changes: 3 additions & 0 deletions Benchmarks/Benchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
using StringExtension;
using StringExtension.Casing;
using StringExtension.Linguistics;
using StringExtension.Validation;

BenchmarkRunner.Run<StringExtensionBenchmark>(
ManualConfig.Create(DefaultConfig.Instance.WithOptions(ConfigOptions.DisableOptimizationsValidator)));
Expand Down
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.5.7</Version>
<Version>2.0.0</Version>
</PropertyGroup>
</Project>
</Project>
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,26 @@ A string processing library for the C# programming language. It provides feature

## Features

> **Breaking change in v2.0.0**: methods are now split across namespaces by category. You'll need to add the relevant `using` statements below instead of a single `using StringExtension;`.

`using StringExtension;`
- RemoveCharacters - removes the specified characters from a string
- IsValidEmail - validate an e-mail address
- IsValidPhoneNumber - validate a phone number
- CountSubstring - counts the number of occurrences of a string in the specified string
- ReverseWords - reverses the order of words in the specified string
- RemoveDuplicateCharacters - remove duplicate characters from a given string

`using StringExtension.Validation;`
- IsValidEmail - validate an e-mail address
- IsValidPhoneNumber - validate a phone number

`using StringExtension.Linguistics;`
- IsPalindrome - determines if a given string is a [palindrome](https://en.wikipedia.org/wiki/Palindrome)
- CountLetters - counts the number of letters in a given string.
- RemoveDuplicateCharacters - remove duplicate characters from a given string.
- ToCamelCase - converts a given string to camel case.
- CountLetters - counts the number of letters in a given string

`using StringExtension.Casing;`
- ToCamelCase - converts a given string to camel case

All methods that classify or compare characters (`IsPalindrome`, `CountLetters`, `ToCamelCase`) operate on whole Unicode scalar values (via [`Rune`](https://learn.microsoft.com/en-us/dotnet/api/system.text.rune)), so characters outside the Basic Multilingual Plane (such as many emoji) are handled correctly.

## Installation

Expand Down
79 changes: 79 additions & 0 deletions StringExtension/Casing/Casing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Buffers;
using System.Text;
using StringExtension.Internal;

namespace StringExtension.Casing;

/// <summary>
/// Provides extension methods for converting the casing of strings.
/// </summary>
public static class Casing
{
/// <summary>
/// Converts the given string to camel case.
/// </summary>
/// <param name="input">The input string.</param>
/// <returns>The input string converted to camel case.</returns>
public static string ToCamelCase(this string input)
{
return ToCamelCase(input.AsSpan());
}

/// <summary>
/// Converts the given span of characters to camel case.
/// </summary>
/// <param name="input">The input characters.</param>
/// <returns>The input converted to camel case.</returns>
/// <remarks>
/// Cases whole Unicode scalar values (<see cref="Rune"/>) rather than UTF-16
/// code units, so characters outside the Basic Multilingual Plane are handled
/// correctly instead of having casing applied to unpaired surrogate halves.
/// </remarks>
public static string ToCamelCase(this ReadOnlySpan<char> input)
{
if (input.IsEmpty)
{
return string.Empty;
}

char[]? pooledBuffer = null;
Span<char> buffer = (uint)input.Length <= BufferLimits.StackAllocThreshold
? stackalloc char[input.Length]
: (pooledBuffer = ArrayPool<char>.Shared.Rent(input.Length));

try
{
var count = 0;
var shouldCapitalize = false;
var isFirstWrittenRune = true;

foreach (var rune in input.EnumerateRunes())
{
if (Rune.IsWhiteSpace(rune) || rune.Value == '_')
{
shouldCapitalize = true;
continue;
}

// The very first character is always lowercase, even if the input
// started with a separator (e.g. "_hello" gives "hello", not "Hello").
var cased = isFirstWrittenRune || !shouldCapitalize
? Rune.ToLowerInvariant(rune)
: Rune.ToUpperInvariant(rune);

count += cased.EncodeToUtf16(buffer[count..]);
shouldCapitalize = false;
isFirstWrittenRune = false;
}

return new string(buffer[..count]);
}
finally
{
if (pooledBuffer is not null)
{
ArrayPool<char>.Shared.Return(pooledBuffer);
}
}
}
}
14 changes: 14 additions & 0 deletions StringExtension/Internal/BufferLimits.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace StringExtension.Internal;

/// <summary>
/// Shared implementation constants. Not part of the public API.
/// </summary>
internal static class BufferLimits
{
/// <summary>
/// Above this length, buffers are rented from <see cref="System.Buffers.ArrayPool{T}"/>
/// instead of stack-allocated, to avoid excessive stack usage for large inputs. 512 chars
/// (1024 bytes) matches the safe stackalloc limit used throughout the .NET base class library.
/// </summary>
internal const int StackAllocThreshold = 512;
}
114 changes: 114 additions & 0 deletions StringExtension/Linguistics/Linguistics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System.Buffers;
using System.Text;
using StringExtension.Internal;

namespace StringExtension.Linguistics;

/// <summary>
/// Provides extension methods for linguistic analysis of strings.
/// </summary>
public static class Linguistics
{
/// <summary>
/// Determines if the given string is a palindrome.
/// </summary>
/// <param name="input">The input string.</param>
/// <returns><c>true</c> if the string is a palindrome; otherwise, <c>false</c>.</returns>
public static bool IsPalindrome(this string input)
{
return IsPalindrome(input.AsSpan());
}

/// <summary>
/// Determines if the given span of characters is a palindrome.
/// </summary>
/// <param name="input">The input characters.</param>
/// <returns><c>true</c> if the characters form a palindrome; otherwise, <c>false</c>.</returns>
/// <remarks>
/// Compares whole Unicode scalar values (<see cref="Rune"/>) rather than UTF-16
/// code units, so multi-char characters (e.g. characters outside the Basic
/// Multilingual Plane, such as many emoji) are compared correctly instead of
/// being split into unpaired surrogate halves.
/// </remarks>
public static bool IsPalindrome(this ReadOnlySpan<char> input)
{
if (input.IsEmpty)
{
return false;
}

Rune[]? pooledBuffer = null;
Span<Rune> letters = input.Length <= BufferLimits.StackAllocThreshold
? stackalloc Rune[input.Length]
: (pooledBuffer = ArrayPool<Rune>.Shared.Rent(input.Length));

try
{
var count = 0;
foreach (var rune in input.EnumerateRunes())
{
if (Rune.IsLetter(rune))
{
letters[count++] = rune;
}
}

var left = 0;
var right = count - 1;

while (left < right)
{
if (Rune.ToLowerInvariant(letters[left]) != Rune.ToLowerInvariant(letters[right]))
{
return false;
}

left++;
right--;
}

return true;
}
finally
{
if (pooledBuffer is not null)
{
ArrayPool<Rune>.Shared.Return(pooledBuffer);
}
}
}

/// <summary>
/// Counts the number of letters in the given string.
/// </summary>
/// <param name="input">The input string.</param>
/// <returns>The number of letters in the input string.</returns>
public static int CountLetters(this string input)
{
return CountLetters(input.AsSpan());
}

/// <summary>
/// Counts the number of letters in the given span of characters.
/// </summary>
/// <param name="input">The input characters.</param>
/// <returns>The number of letters in the input.</returns>
/// <remarks>
/// Classifies whole Unicode scalar values (<see cref="Rune"/>) rather than UTF-16
/// code units, so letters outside the Basic Multilingual Plane are counted
/// correctly instead of being missed as unpaired surrogate halves.
/// </remarks>
public static int CountLetters(this ReadOnlySpan<char> input)
{
var count = 0;
foreach (var rune in input.EnumerateRunes())
{
if (Rune.IsLetter(rune))
{
count++;
}
}

return count;
}
}
Loading
Loading