Skip to content

Commit

Permalink
feat: add humanize and pascalize string exts
Browse files Browse the repository at this point in the history
  • Loading branch information
elringus committed Oct 6, 2024
1 parent 5cda2cd commit 2694903
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 0 deletions.
63 changes: 63 additions & 0 deletions backend/Naninovel.Common.Modern/Utilities/TextUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
namespace Naninovel;

/// <summary>
/// Common text-related helpers and extensions.
/// </summary>
public static class TextUtils
{
/// <summary>
/// Formats specified string for human readability by removing non-letter and
/// non-digit characters and converting from snake_case, PascalCase, camelCase,
/// kebab-case or SCREAMING_CASE to Title Case.
/// </summary>
public static string Humanize (this string str)
{
int idx, length = 0;
char curr, prev, next, last = default;
Span<char> buffer = stackalloc char[str.Length * 2];

for (idx = 0; idx < str.Length; idx++)
{
curr = str[idx];
prev = idx > 0 ? str[idx - 1] : default;
next = idx + 1 < str.Length ? str[idx + 1] : default;
last = length > 0 ? buffer[length - 1] : default;
if (!Skip()) buffer[length++] = Upper() ? char.ToUpper(curr) : Lower() ? char.ToLower(curr) : curr;
if (Space()) buffer[length++] = ' ';
}

return buffer[..length].ToString();

bool Skip () => !char.IsLetterOrDigit(curr);
bool Upper () => length == 1 || char.IsWhiteSpace(last);
bool Lower () => char.IsUpper(curr) && char.IsUpper(prev);
bool Space () => length > 0 && (
char.IsLower(curr) && char.IsUpper(next) ||
char.IsLetter(curr) && char.IsDigit(next) ||
char.IsDigit(curr) && char.IsLetter(next) ||
!char.IsLetterOrDigit(curr) && char.IsLetterOrDigit(next));
}

/// <summary>
/// Converts specified string to PascalCase.
/// </summary>
public static string ToPascalCase (this string str)
{
int idx, length = 0;
char curr, prev = default;
Span<char> buffer = stackalloc char[str.Length];

for (idx = 0; idx < str.Length; idx++)
{
curr = str[idx];
prev = idx > 0 ? str[idx - 1] : default;
if (!Skip()) buffer[length++] = Upper() ? char.ToUpper(curr) : Lower() ? char.ToLower(curr) : curr;
}

return buffer[..length].ToString();

bool Skip () => !char.IsLetterOrDigit(curr);
bool Upper () => length == 1 || !char.IsLetterOrDigit(prev);
bool Lower () => char.IsUpper(curr) && char.IsUpper(prev);
}
}
52 changes: 52 additions & 0 deletions backend/Naninovel.Common.Test/Utilities/TextUtilsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,56 @@ public void SanitizeWorksCorrectly (string input, IReadOnlyCollection<char> inva
{
Assert.Equal(expected, input.Sanitize(invalid));
}

[Theory]
[InlineData("", "")]
[InlineData(" ", "")]
[InlineData("_-\\+^@", "")]
[InlineData("FooBar", "Foo Bar")]
[InlineData("fooBar", "Foo Bar")]
[InlineData("foo_bar", "Foo Bar")]
[InlineData("FOO_BAR", "Foo Bar")]
[InlineData("FOO BAR", "Foo Bar")]
[InlineData("foo-bar", "Foo Bar")]
[InlineData("Foo Bar", "Foo Bar")]
[InlineData("Foo1", "Foo 1")]
[InlineData("Foo 1", "Foo 1")]
[InlineData("Foo123Bar321Nya", "Foo 123 Bar 321 Nya")]
[InlineData("Foo 123Bar", "Foo 123 Bar")]
[InlineData(" Foo 123 Bar ", "Foo 123 Bar")]
[InlineData("Foo-123_Bar", "Foo 123 Bar")]
[InlineData("_foo_", "Foo")]
[InlineData("__foo__", "Foo")]
[InlineData("___foo___", "Foo")]
[InlineData(" _ __ FOO1-2-3BAR __ _ ", "Foo 1 2 3 Bar")]
public void HumanizeWorksCorrectly (string input, string expected)
{
Assert.Equal(expected, input.Humanize());
}

[Theory]
[InlineData("", "")]
[InlineData(" ", "")]
[InlineData("_-\\+^@", "")]
[InlineData("FooBar", "FooBar")]
[InlineData("fooBar", "FooBar")]
[InlineData("foo_bar", "FooBar")]
[InlineData("FOO_BAR", "FooBar")]
[InlineData("FOO BAR", "FooBar")]
[InlineData("foo-bar", "FooBar")]
[InlineData("Foo Bar", "FooBar")]
[InlineData("Foo1", "Foo1")]
[InlineData("Foo 1", "Foo1")]
[InlineData("Foo 123 Bar 321 Nya", "Foo123Bar321Nya")]
[InlineData("Foo 123Bar", "Foo123Bar")]
[InlineData(" Foo 123 Bar ", "Foo123Bar")]
[InlineData("Foo-123_Bar", "Foo123Bar")]
[InlineData("_foo_", "Foo")]
[InlineData("__foo__", "Foo")]
[InlineData("___foo___", "Foo")]
[InlineData(" _ __ FOO1-2-3BAR __ _ ", "Foo123Bar")]
public void ToPascalCaseWorksCorrectly (string input, string expected)
{
Assert.Equal(expected, input.ToPascalCase());
}
}

0 comments on commit 2694903

Please sign in to comment.