From 3980c970887b023e7c3d533b932a90f8ab79a55f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 27 May 2024 22:50:04 -0400 Subject: [PATCH] Replace relevant Substring use with AsSpan --- src/Humanizer/Humanizer.csproj | 1 + src/Humanizer/Inflections/Vocabulary.cs | 4 +- src/Humanizer/InflectorExtensions.cs | 8 ++-- .../Formatters/LuxembourgishFormatter.cs | 10 ++--- .../ArabicNumberToWordsConverter.cs | 10 +++-- .../AzerbaijaniNumberToWordsConverter.cs | 4 +- ...azilianPortugueseNumberToWordsConverter.cs | 12 +++--- .../DutchNumberToWordsConverter.cs | 4 +- .../LuxembourgishNumberToWordsConverter.cs | 4 +- .../PortugueseNumberToWordsConverter.cs | 8 ++-- .../RomanianOrdinalNumberConverter.cs | 13 ++++--- .../TurkishNumberToWordConverter.cs | 4 +- src/Humanizer/StringHumanizeExtensions.cs | 37 +++++++++++++++++-- src/Humanizer/Transformer/ToSentenceCase.cs | 9 ++++- .../Truncation/FixedLengthTruncator.cs | 6 +-- .../FixedNumberOfCharactersTruncator.cs | 4 +- .../Truncation/FixedNumberOfWordsTruncator.cs | 4 +- 17 files changed, 93 insertions(+), 49 deletions(-) diff --git a/src/Humanizer/Humanizer.csproj b/src/Humanizer/Humanizer.csproj index 07cc49d34..b66456160 100644 --- a/src/Humanizer/Humanizer.csproj +++ b/src/Humanizer/Humanizer.csproj @@ -12,6 +12,7 @@ embedded CS1573;CS1591 true + true diff --git a/src/Humanizer/Inflections/Vocabulary.cs b/src/Humanizer/Inflections/Vocabulary.cs index 392f96eea..c818a864f 100644 --- a/src/Humanizer/Inflections/Vocabulary.cs +++ b/src/Humanizer/Inflections/Vocabulary.cs @@ -1,4 +1,4 @@ -namespace Humanizer; +namespace Humanizer; /// /// A container for exceptions to simple pluralization/singularization rules. @@ -185,7 +185,7 @@ bool IsUncountable(string word) => static string MatchUpperCase(string word, string replacement) => char.IsUpper(word[0]) && - char.IsLower(replacement[0]) ? char.ToUpper(replacement[0]) + replacement.Substring(1) : replacement; + char.IsLower(replacement[0]) ? StringHumanizeExtensions.Concat(char.ToUpper(replacement[0]), replacement.AsSpan(1)) : replacement; /// /// If the word is the letter s, singular or plural, return the letter s singular diff --git a/src/Humanizer/InflectorExtensions.cs b/src/Humanizer/InflectorExtensions.cs index 1f082d23a..b129372e8 100644 --- a/src/Humanizer/InflectorExtensions.cs +++ b/src/Humanizer/InflectorExtensions.cs @@ -1,4 +1,4 @@ -//The Inflector class was cloned from Inflector (https://github.com/srkirkland/Inflector) +//The Inflector class was cloned from Inflector (https://github.com/srkirkland/Inflector) //The MIT License (MIT) @@ -65,9 +65,9 @@ public static string Camelize(this string input) { var word = input.Pascalize(); return word.Length > 0 - ? word - .Substring(0, 1) - .ToLower() + word.Substring(1) + ? StringHumanizeExtensions.Concat( + char.ToLower(word[0]), + word.AsSpan(1)) : word; } diff --git a/src/Humanizer/Localisation/Formatters/LuxembourgishFormatter.cs b/src/Humanizer/Localisation/Formatters/LuxembourgishFormatter.cs index a5ca34a8f..eb5f46d93 100644 --- a/src/Humanizer/Localisation/Formatters/LuxembourgishFormatter.cs +++ b/src/Humanizer/Localisation/Formatters/LuxembourgishFormatter.cs @@ -1,4 +1,4 @@ -namespace Humanizer; +namespace Humanizer; class LuxembourgishFormatter(CultureInfo culture) : DefaultFormatter(culture) @@ -18,12 +18,12 @@ public static string ApplyEifelerRule(string word) => word.TrimEnd(EifelerRuleSuffix); public static string CheckForAndApplyEifelerRule(string word, string nextWord) - => DoesEifelerRuleApply(nextWord) + => DoesEifelerRuleApply(nextWord.AsSpan()) ? word.TrimEnd(EifelerRuleSuffix) : word; - public static bool DoesEifelerRuleApply(string nextWord) - => !string.IsNullOrWhiteSpace(nextWord) + public static bool DoesEifelerRuleApply(CharSpan nextWord) + => !nextWord.IsWhiteSpace() && !EifelerRuleCharacters.Contains(nextWord[0]); protected override string Format(TimeUnit unit, string resourceKey, int number, bool toWords = false) @@ -33,7 +33,7 @@ protected override string Format(TimeUnit unit, string resourceKey, int number, return string.Format(resourceString, toWords ? numberAsWord : number, - DoesEifelerRuleApply(numberAsWord) ? "" : EifelerRuleSuffix); + DoesEifelerRuleApply(numberAsWord.AsSpan()) ? "" : EifelerRuleSuffix); } protected override string GetResourceKey(string resourceKey, int number) diff --git a/src/Humanizer/Localisation/NumberToWords/ArabicNumberToWordsConverter.cs b/src/Humanizer/Localisation/NumberToWords/ArabicNumberToWordsConverter.cs index 250c7fb35..7f373eea4 100644 --- a/src/Humanizer/Localisation/NumberToWords/ArabicNumberToWordsConverter.cs +++ b/src/Humanizer/Localisation/NumberToWords/ArabicNumberToWordsConverter.cs @@ -1,4 +1,4 @@ -namespace Humanizer; +namespace Humanizer; class ArabicNumberToWordsConverter : GenderedNumberToWordsConverter @@ -268,7 +268,9 @@ static string ParseNumber(string word, int number, GrammaticalGender gender) foreach (var kv in ordinals.Where(kv => word.EndsWith(kv.Key))) { // replace word with exception - return word.Substring(0, word.Length - kv.Key.Length) + kv.Value; + return StringHumanizeExtensions.Concat( + word.AsSpan(0, word.Length - kv.Key.Length), + kv.Value.AsSpan()); } } else if (number is > 10 and < 100) @@ -286,7 +288,9 @@ static string ParseNumber(string word, int number, GrammaticalGender gender) foreach (var kv in ordinals.Where(kv => oldPart.EndsWith(kv.Key))) { // replace word with exception - newPart = oldPart.Substring(0, oldPart.Length - kv.Key.Length) + kv.Value; + newPart = StringHumanizeExtensions.Concat( + oldPart.AsSpan(0, oldPart.Length - kv.Key.Length), + kv.Value.AsSpan()); } if (number > 19 && newPart == oldPart && oldPart.Length > 1) diff --git a/src/Humanizer/Localisation/NumberToWords/AzerbaijaniNumberToWordsConverter.cs b/src/Humanizer/Localisation/NumberToWords/AzerbaijaniNumberToWordsConverter.cs index 17f98ac24..1ebc9e37c 100644 --- a/src/Humanizer/Localisation/NumberToWords/AzerbaijaniNumberToWordsConverter.cs +++ b/src/Humanizer/Localisation/NumberToWords/AzerbaijaniNumberToWordsConverter.cs @@ -1,4 +1,4 @@ -namespace Humanizer; +namespace Humanizer; class AzerbaijaniNumberToWordsConverter : GenderlessNumberToWordsConverter @@ -116,7 +116,7 @@ public override string ConvertToOrdinal(int number) if (word[^1] == 't') { - word = word.Substring(0, word.Length - 1) + 'd'; + word = StringHumanizeExtensions.Concat(word.AsSpan(0, word.Length - 1), 'd'); } if (suffixFoundOnLastVowel) diff --git a/src/Humanizer/Localisation/NumberToWords/BrazilianPortugueseNumberToWordsConverter.cs b/src/Humanizer/Localisation/NumberToWords/BrazilianPortugueseNumberToWordsConverter.cs index 9e3b5d8fe..f45dc6921 100644 --- a/src/Humanizer/Localisation/NumberToWords/BrazilianPortugueseNumberToWordsConverter.cs +++ b/src/Humanizer/Localisation/NumberToWords/BrazilianPortugueseNumberToWordsConverter.cs @@ -1,4 +1,4 @@ -namespace Humanizer; +namespace Humanizer; class BrazilianPortugueseNumberToWordsConverter : GenderedNumberToWordsConverter @@ -166,17 +166,17 @@ static string ApplyGender(string toWords, GrammaticalGender gender) if (toWords.EndsWith("os")) { - return toWords.Substring(0, toWords.Length - 2) + "as"; + return StringHumanizeExtensions.Concat(toWords.AsSpan(0, toWords.Length - 2), "as".AsSpan()); } if (toWords.EndsWith("um")) { - return toWords.Substring(0, toWords.Length - 2) + "uma"; + return StringHumanizeExtensions.Concat(toWords.AsSpan(0, toWords.Length - 2), "uma".AsSpan()); } if (toWords.EndsWith("dois")) { - return toWords.Substring(0, toWords.Length - 4) + "duas"; + return StringHumanizeExtensions.Concat(toWords.AsSpan(0, toWords.Length - 4), "duas".AsSpan()); } return toWords; @@ -186,7 +186,9 @@ static string ApplyOrdinalGender(string toWords, GrammaticalGender gender) { if (gender == GrammaticalGender.Feminine) { - return toWords.TrimEnd('o') + 'a'; + return StringHumanizeExtensions.Concat( + toWords.AsSpan().TrimEnd('o'), + 'a'); } return toWords; diff --git a/src/Humanizer/Localisation/NumberToWords/DutchNumberToWordsConverter.cs b/src/Humanizer/Localisation/NumberToWords/DutchNumberToWordsConverter.cs index 30239fad8..7f3904ce6 100644 --- a/src/Humanizer/Localisation/NumberToWords/DutchNumberToWordsConverter.cs +++ b/src/Humanizer/Localisation/NumberToWords/DutchNumberToWordsConverter.cs @@ -1,4 +1,4 @@ -namespace Humanizer; +namespace Humanizer; /// /// Dutch spelling of numbers is not really officially regulated. @@ -170,7 +170,7 @@ public override string ConvertToOrdinal(int number) foreach (var kv in OrdinalExceptions.Where(kv => word.EndsWith(kv.Key))) { // replace word with exception - return word.Substring(0, word.Length - kv.Key.Length) + kv.Value; + return StringHumanizeExtensions.Concat(word.AsSpan(0, word.Length - kv.Key.Length), kv.Value.AsSpan()); } // achtste diff --git a/src/Humanizer/Localisation/NumberToWords/LuxembourgishNumberToWordsConverter.cs b/src/Humanizer/Localisation/NumberToWords/LuxembourgishNumberToWordsConverter.cs index 7cf6a74ae..d2256e34a 100644 --- a/src/Humanizer/Localisation/NumberToWords/LuxembourgishNumberToWordsConverter.cs +++ b/src/Humanizer/Localisation/NumberToWords/LuxembourgishNumberToWordsConverter.cs @@ -1,4 +1,4 @@ -namespace Humanizer; +namespace Humanizer; class LuxembourgishNumberToWordsConverter : GenderedNumberToWordsConverter { @@ -166,7 +166,7 @@ private static string GetEndingForGender(GrammaticalGender gender) => private string GetPartWithEifelerRule(string pluralFormat, long number, GrammaticalGender gender) { var nextWord = pluralFormat - .Substring(3, pluralFormat.Length - 3) + .AsSpan(3, pluralFormat.Length - 3) .TrimStart(); var wordForm = LuxembourgishFormatter.DoesEifelerRuleApply(nextWord) ? WordForm.Eifeler diff --git a/src/Humanizer/Localisation/NumberToWords/PortugueseNumberToWordsConverter.cs b/src/Humanizer/Localisation/NumberToWords/PortugueseNumberToWordsConverter.cs index ca3331a5e..ac560bec6 100644 --- a/src/Humanizer/Localisation/NumberToWords/PortugueseNumberToWordsConverter.cs +++ b/src/Humanizer/Localisation/NumberToWords/PortugueseNumberToWordsConverter.cs @@ -1,4 +1,4 @@ -namespace Humanizer; +namespace Humanizer; class PortugueseNumberToWordsConverter : GenderedNumberToWordsConverter { @@ -165,17 +165,17 @@ static string ApplyGender(string toWords, GrammaticalGender gender) if (toWords.EndsWith("os")) { - return toWords.Substring(0, toWords.Length - 2) + "as"; + return StringHumanizeExtensions.Concat(toWords.AsSpan(0, toWords.Length - 2), "as".AsSpan()); } if (toWords.EndsWith("um")) { - return toWords.Substring(0, toWords.Length - 2) + "uma"; + return StringHumanizeExtensions.Concat(toWords.AsSpan(0, toWords.Length - 2), "uma".AsSpan()); } if (toWords.EndsWith("dois")) { - return toWords.Substring(0, toWords.Length - 4) + "duas"; + return StringHumanizeExtensions.Concat(toWords.AsSpan(0, toWords.Length - 4), "duas".AsSpan()); } return toWords; diff --git a/src/Humanizer/Localisation/NumberToWords/Romanian/RomanianOrdinalNumberConverter.cs b/src/Humanizer/Localisation/NumberToWords/Romanian/RomanianOrdinalNumberConverter.cs index 476af2e3d..30d40506b 100644 --- a/src/Humanizer/Localisation/NumberToWords/Romanian/RomanianOrdinalNumberConverter.cs +++ b/src/Humanizer/Localisation/NumberToWords/Romanian/RomanianOrdinalNumberConverter.cs @@ -1,4 +1,4 @@ -namespace Humanizer; +namespace Humanizer; class RomanianOrdinalNumberConverter { @@ -72,7 +72,7 @@ public string Convert(int number, GrammaticalGender gender) if (gender == GrammaticalGender.Feminine && words.EndsWith("zeci")) { - words = words.Substring(0, words.Length - 4) + "zece"; + words = StringHumanizeExtensions.Concat(words.AsSpan(0, words.Length - 4), "zece".AsSpan()); } else if (gender == GrammaticalGender.Feminine && words.Contains("zeci") && (words.Contains("milioane") || words.Contains("miliarde"))) { @@ -82,15 +82,16 @@ public string Convert(int number, GrammaticalGender gender) if (gender == GrammaticalGender.Feminine && words.StartsWith("un ")) { words = words - .Substring(2) - .TrimStart(); + .AsSpan(2) + .TrimStart() + .ToString(); } if (words.EndsWith("milioane")) { if (gender == GrammaticalGender.Feminine) { - words = words.Substring(0, words.Length - 8) + "milioana"; + words = StringHumanizeExtensions.Concat(words.AsSpan(0, words.Length - 8), "milioana".AsSpan()); } } @@ -99,7 +100,7 @@ public string Convert(int number, GrammaticalGender gender) { if (gender == GrammaticalGender.Feminine) { - words = words.Substring(0, words.Length - 6) + "milioana"; + words = StringHumanizeExtensions.Concat(words.AsSpan(0, words.Length - 6), "milioana".AsSpan()); } else { diff --git a/src/Humanizer/Localisation/NumberToWords/TurkishNumberToWordConverter.cs b/src/Humanizer/Localisation/NumberToWords/TurkishNumberToWordConverter.cs index dfe8d3793..041ab936e 100644 --- a/src/Humanizer/Localisation/NumberToWords/TurkishNumberToWordConverter.cs +++ b/src/Humanizer/Localisation/NumberToWords/TurkishNumberToWordConverter.cs @@ -1,4 +1,4 @@ -namespace Humanizer; +namespace Humanizer; class TurkishNumberToWordConverter : GenderlessNumberToWordsConverter @@ -154,7 +154,7 @@ public override string ConvertToOrdinal(int number) if (word[^1] == 't') { - word = word.Substring(0, word.Length - 1) + 'd'; + word = StringHumanizeExtensions.Concat(word.AsSpan(0, word.Length - 1), 'd'); } if (suffixFoundOnLastVowel) diff --git a/src/Humanizer/StringHumanizeExtensions.cs b/src/Humanizer/StringHumanizeExtensions.cs index 5da760deb..99eb5c0a0 100644 --- a/src/Humanizer/StringHumanizeExtensions.cs +++ b/src/Humanizer/StringHumanizeExtensions.cs @@ -1,4 +1,8 @@ -namespace Humanizer; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Humanizer; /// /// Contains extension methods for humanizing string values. @@ -49,8 +53,7 @@ static string FromPascalCase(string input) } return result.Length > 0 - ? char.ToUpper(result[0]) + - result.Substring(1, result.Length - 1) + ? Concat(char.ToUpper(result[0]), result.AsSpan(1, result.Length - 1)) : result; } @@ -90,4 +93,32 @@ public static string Humanize(this string input, LetterCasing casing) => input .Humanize() .ApplyCase(casing); + +#if NET + internal static string Concat(CharSpan left, CharSpan right) => + string.Concat(left, right); + + internal static string Concat(char left, CharSpan right) => + string.Concat(MemoryMarshal.CreateReadOnlySpan(ref left, 1), right); + + internal static string Concat(CharSpan left, char right) => + string.Concat(left, MemoryMarshal.CreateReadOnlySpan(ref right, 1)); +#else + internal static unsafe string Concat(CharSpan left, CharSpan right) + { + var result = new string('\0', left.Length + right.Length); + fixed (char* pResult = result) + { + left.CopyTo(new Span(pResult, left.Length)); + right.CopyTo(new Span(pResult + left.Length, right.Length)); + } + return result; + } + + internal static unsafe string Concat(char left, CharSpan right) => + Concat(new CharSpan(&left, 1), right); + + internal static unsafe string Concat(CharSpan left, char right) => + Concat(left, new CharSpan(&right, 1)); +#endif } \ No newline at end of file diff --git a/src/Humanizer/Transformer/ToSentenceCase.cs b/src/Humanizer/Transformer/ToSentenceCase.cs index db5dbfcac..7d4317920 100644 --- a/src/Humanizer/Transformer/ToSentenceCase.cs +++ b/src/Humanizer/Transformer/ToSentenceCase.cs @@ -1,4 +1,4 @@ -namespace Humanizer; +namespace Humanizer; class ToSentenceCase : ICulturedStringTransformer { @@ -11,7 +11,12 @@ public string Transform(string input, CultureInfo? culture) if (input.Length >= 1) { - return culture.TextInfo.ToUpper(input[0]) + input.Substring(1); + if (char.IsUpper(input[0])) + { + return input; + } + + return StringHumanizeExtensions.Concat(culture.TextInfo.ToUpper(input[0]), input.AsSpan(1)); } return culture.TextInfo.ToUpper(input); diff --git a/src/Humanizer/Truncation/FixedLengthTruncator.cs b/src/Humanizer/Truncation/FixedLengthTruncator.cs index cea07bd98..42af71d29 100644 --- a/src/Humanizer/Truncation/FixedLengthTruncator.cs +++ b/src/Humanizer/Truncation/FixedLengthTruncator.cs @@ -1,4 +1,4 @@ -namespace Humanizer; +namespace Humanizer; /// /// Truncate a string to a fixed length @@ -28,12 +28,12 @@ class FixedLengthTruncator : ITruncator if (truncateFrom == TruncateFrom.Left) { return value.Length > length - ? truncationString + value.Substring(value.Length - length + truncationString.Length) + ? StringHumanizeExtensions.Concat(truncationString.AsSpan(), value.AsSpan(value.Length - length + truncationString.Length)) : value; } return value.Length > length - ? value.Substring(0, length - truncationString.Length) + truncationString + ? StringHumanizeExtensions.Concat(value.AsSpan(0, length - truncationString.Length), truncationString.AsSpan()) : value; } } \ No newline at end of file diff --git a/src/Humanizer/Truncation/FixedNumberOfCharactersTruncator.cs b/src/Humanizer/Truncation/FixedNumberOfCharactersTruncator.cs index a53453094..7af77ede6 100644 --- a/src/Humanizer/Truncation/FixedNumberOfCharactersTruncator.cs +++ b/src/Humanizer/Truncation/FixedNumberOfCharactersTruncator.cs @@ -43,7 +43,7 @@ class FixedNumberOfCharactersTruncator : ITruncator if (alphaNumericalCharactersProcessed + truncationString.Length == length) { - return truncationString + value.Substring(i); + return StringHumanizeExtensions.Concat(truncationString.AsSpan(), value.AsSpan(i)); } } } @@ -57,7 +57,7 @@ class FixedNumberOfCharactersTruncator : ITruncator if (alphaNumericalCharactersProcessed + truncationString.Length == length) { - return value.Substring(0, i + 1) + truncationString; + return StringHumanizeExtensions.Concat(value.AsSpan(0, i + 1), truncationString.AsSpan()); } } diff --git a/src/Humanizer/Truncation/FixedNumberOfWordsTruncator.cs b/src/Humanizer/Truncation/FixedNumberOfWordsTruncator.cs index 4cb6b7e4b..5d3a3d56b 100644 --- a/src/Humanizer/Truncation/FixedNumberOfWordsTruncator.cs +++ b/src/Humanizer/Truncation/FixedNumberOfWordsTruncator.cs @@ -46,7 +46,7 @@ static string TruncateFromRight(string value, int length, string? truncationStri if (numberOfWordsProcessed == length) { - return value.Substring(0, i) + truncationString; + return StringHumanizeExtensions.Concat(value.AsSpan(0, i), truncationString.AsSpan()); } } else @@ -74,7 +74,7 @@ static string TruncateFromLeft(string value, int length, string? truncationStrin if (numberOfWordsProcessed == length) { - return truncationString + value.Substring(i + 1).TrimEnd(); + return StringHumanizeExtensions.Concat(truncationString.AsSpan(), value.AsSpan(i + 1).TrimEnd()); } } else