// MIT License - Copyright (c) 2023 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Extension { using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Text; using Serialization; using WallstopStudios.UnityHelpers.Utils; /// /// Defines string casing formats for text transformation. /// public enum StringCase { /// Invalid string case placeholder. [Obsolete("Please use a valid StringCase enum value.")] None = 0, /// PascalCase - FirstLetterCapitalized for each word, no separators (e.g., "HelloWorld"). PascalCase = 1, /// camelCase - first letter lowercase, subsequent words capitalized, no separators (e.g., "helloWorld"). CamelCase = 2, /// snake_case - all lowercase with underscores between words (e.g., "hello_world"). SnakeCase = 3, /// kebab-case - all lowercase with hyphens between words (e.g., "hello-world"). KebabCase = 4, /// Title Case - Each Word Capitalized With Spaces (e.g., "Hello World"). TitleCase = 5, /// lowercase - all characters converted to lowercase (e.g., "hello world"). LowerCase = 6, /// UPPERCASE - all characters converted to uppercase (e.g., "HELLO WORLD"). UpperCase = 7, /// lowercase invariant - culture-invariant lowercase conversion. LowerInvariant = 8, /// UPPERCASE INVARIANT - culture-invariant uppercase conversion. UpperInvariant = 9, } /// /// Extension methods for string manipulation including case conversion, encoding, serialization, and text analysis. /// /// /// Thread Safety: All methods are thread-safe as they operate on immutable strings. /// Performance: Methods use StringBuilder and pooled buffers for efficiency where possible. /// Allocations: Most methods allocate new strings; some use pooled resources to minimize intermediate allocations. /// public static class StringExtensions { private static readonly ImmutableHashSet WordSeparators = new HashSet { '_', '-', ' ', '\r', '\n', '\t', '.', '"', }.ToImmutableHashSet(); private static readonly ImmutableHashSet CharsToStrip = new HashSet { '\'', }.ToImmutableHashSet(); private const char CombiningDotAbove = '\u0307'; private const char CapitalIWithDot = '\u0130'; private static readonly string CombiningDotAboveString = CombiningDotAbove.ToString(); private static readonly string CapitalIWithDotString = CapitalIWithDot.ToString(); private enum CharacterCategory { None, Lower, Upper, Digit, Other, } private enum CaseTokenKind { Word, Separator, } private readonly struct CaseToken { public CaseToken( CaseTokenKind kind, string value, bool hasLetter, bool hasDigit, bool hasUppercase ) { Kind = kind; Value = value; HasLetter = hasLetter; HasDigit = hasDigit; HasUppercase = hasUppercase; } public CaseTokenKind Kind { get; } public string Value { get; } public bool HasLetter { get; } public bool HasDigit { get; } public bool HasUppercase { get; } public bool IsNumeric => !HasLetter && HasDigit; } /// /// Centers a string within a field of the specified total length by padding spaces on both sides. /// /// The string to center. /// The total width of the resulting string. /// /// The centered string when exceeds the input length; otherwise the original string. /// /// /// Returns the original string when is less than or equal to the input length. /// Uses space characters for padding; does not truncate. /// /// /// /// string s = "hi"; /// string centered = s.Center(6); // " hi " /// /// public static string Center(this string input, int length) { if (input == null || length <= input.Length) { return input; } return input.PadLeft((length - input.Length) / 2 + input.Length).PadRight(length); } /// /// Converts a string to its UTF-8 byte array representation. /// /// The string to convert. /// A byte array containing the UTF-8 encoded bytes, or an empty array if input is null or empty. /// /// Null handling: Returns Array.Empty<byte>() if input is null or empty. /// Thread-safe: Yes. /// Performance: O(n) where n is the string length. /// Allocations: Allocates a new byte array. Returns cached empty array for null/empty input. /// Edge cases: Empty or null strings return empty array. /// public static byte[] GetBytes(this string input) { if (string.IsNullOrEmpty(input)) { return Array.Empty(); } return Encoding.UTF8.GetBytes(input); } /// /// Converts a UTF-8 byte array to a string. /// /// The byte array to convert. /// The decoded string, or an empty string if bytes is null or empty. /// /// Null handling: Returns string.Empty if bytes is null or empty. /// Thread-safe: Yes. /// Performance: O(n) where n is the byte array length. /// Allocations: Allocates a new string. /// Edge cases: Empty or null byte arrays return empty string. /// public static string GetString(this byte[] bytes) { if (bytes == null || bytes.Length == 0) { return string.Empty; } return Encoding.UTF8.GetString(bytes); } /// /// Serializes an object to a JSON string representation. /// /// The type of the value to serialize. /// The value to serialize to JSON. /// A JSON string representation of the value. /// /// Null handling: Behavior depends on Serializer.JsonStringify implementation. /// Thread-safe: Yes. /// Performance: O(n) where n is the complexity of the object graph. /// Allocations: Allocates new string and intermediate serialization structures. /// Edge cases: Complex object graphs may serialize with circular reference handling depending on serializer. /// public static string ToJson(this T value) { return Serializer.JsonStringify(value); } public static int LevenshteinDistance(this string source1, string source2) { source1 ??= string.Empty; source2 ??= string.Empty; int len1 = source1.Length; int len2 = source2.Length; if (len1 == 0) { return len2; } if (len2 == 0) { return len1; } using PooledArray prevLease = SystemArrayPool.Get(len2 + 1, out int[] prev); using PooledArray currLease = SystemArrayPool.Get(len2 + 1, out int[] curr); for (int j = 0; j <= len2; ++j) { prev[j] = j; } for (int i = 1; i <= len1; ++i) { curr[0] = i; char c1 = source1[i - 1]; for (int j = 1; j <= len2; ++j) { int cost = source2[j - 1] == c1 ? 0 : 1; int deletion = prev[j] + 1; int insertion = curr[j - 1] + 1; int substitution = prev[j - 1] + cost; int min = deletion < insertion ? deletion : insertion; curr[j] = min < substitution ? min : substitution; } // swap prev and curr int[] tmp = prev; prev = curr; curr = tmp; } return prev[len2]; } private static List TokenizeForCase(string value) { List tokens = new(); if (string.IsNullOrEmpty(value)) { return tokens; } using PooledResource bufferResource = Buffers.GetStringBuilder( value.Length, out StringBuilder buffer ); CaseTokenKind? currentKind = null; CharacterCategory lastCategory = CharacterCategory.None; for (int i = 0; i < value.Length; ++i) { char current = value[i]; bool isSeparator = WordSeparators.Contains(current) || char.IsWhiteSpace(current); if (isSeparator) { if (currentKind == CaseTokenKind.Word && buffer.Length > 0) { tokens.Add(CreateWordToken(buffer.ToString())); buffer.Clear(); } if (currentKind != CaseTokenKind.Separator) { if (currentKind == CaseTokenKind.Separator && buffer.Length > 0) { tokens.Add( new CaseToken( CaseTokenKind.Separator, buffer.ToString(), false, false, false ) ); buffer.Clear(); } currentKind = CaseTokenKind.Separator; buffer.Clear(); } _ = buffer.Append(current); lastCategory = CharacterCategory.None; continue; } CharacterCategory category = CategorizeChar(current); if (currentKind == CaseTokenKind.Separator && buffer.Length > 0) { tokens.Add( new CaseToken( CaseTokenKind.Separator, buffer.ToString(), false, false, false ) ); buffer.Clear(); } if (currentKind != CaseTokenKind.Word) { currentKind = CaseTokenKind.Word; lastCategory = CharacterCategory.None; } else if (ShouldStartNewWord(category, lastCategory, value, i) && buffer.Length > 0) { tokens.Add(CreateWordToken(buffer.ToString())); buffer.Clear(); lastCategory = CharacterCategory.None; } _ = buffer.Append(current); if (category != CharacterCategory.Other) { lastCategory = category; } } if (currentKind == CaseTokenKind.Separator && buffer.Length > 0) { tokens.Add( new CaseToken(CaseTokenKind.Separator, buffer.ToString(), false, false, false) ); } else if (currentKind == CaseTokenKind.Word && buffer.Length > 0) { tokens.Add(CreateWordToken(buffer.ToString())); } return tokens; } private static CharacterCategory CategorizeChar(char value) { if (char.IsDigit(value)) { return CharacterCategory.Digit; } if (char.IsLetter(value)) { return char.IsUpper(value) ? CharacterCategory.Upper : CharacterCategory.Lower; } return CharacterCategory.Other; } private static bool ShouldStartNewWord( CharacterCategory currentCategory, CharacterCategory lastCategory, string value, int index ) { if ( lastCategory == CharacterCategory.None || currentCategory == CharacterCategory.Other ) { return false; } if (currentCategory == CharacterCategory.Upper) { if ( lastCategory == CharacterCategory.Lower || lastCategory == CharacterCategory.Digit ) { return true; } if (lastCategory == CharacterCategory.Upper) { int nextIndex = index + 1; if (nextIndex < value.Length && char.IsLower(value[nextIndex])) { return true; } } } bool lastWasDigit = lastCategory == CharacterCategory.Digit; bool currentIsDigit = currentCategory == CharacterCategory.Digit; bool lastWasLetter = lastCategory == CharacterCategory.Lower || lastCategory == CharacterCategory.Upper; bool currentIsLetter = currentCategory == CharacterCategory.Lower || currentCategory == CharacterCategory.Upper; if (currentIsDigit && lastWasLetter) { return true; } if (currentIsLetter && lastWasDigit) { return currentCategory == CharacterCategory.Upper; } return false; } private static CaseToken CreateWordToken(string value) { bool hasLetter = false; bool hasDigit = false; bool hasUppercase = false; for (int i = 0; i < value.Length; ++i) { char c = value[i]; if (char.IsDigit(c)) { hasDigit = true; } else if (char.IsLetter(c)) { hasLetter = true; if (char.IsUpper(c)) { hasUppercase = true; } } } return new CaseToken(CaseTokenKind.Word, value, hasLetter, hasDigit, hasUppercase); } private static string SanitizeWord(string value, bool removeStripChars) { if (string.IsNullOrEmpty(value) || !removeStripChars) { return value; } bool needsSanitization = false; for (int i = 0; i < value.Length; ++i) { if (CharsToStrip.Contains(value[i])) { needsSanitization = true; break; } } if (!needsSanitization) { return value; } using PooledResource builderResource = Buffers.GetStringBuilder( value.Length, out StringBuilder builder ); for (int i = 0; i < value.Length; ++i) { char c = value[i]; if (!CharsToStrip.Contains(c)) { _ = builder.Append(c); } } return builder.ToString(); } private static void AppendWordWithCasing( StringBuilder builder, string word, bool uppercaseFirstLetter ) { if (string.IsNullOrEmpty(word)) { return; } char firstChar = word[0]; _ = builder.Append( uppercaseFirstLetter ? char.ToUpperInvariant(firstChar) : char.ToLowerInvariant(firstChar) ); for (int i = 1; i < word.Length; ++i) { char c = word[i]; _ = builder.Append(char.ToLowerInvariant(c)); } } private static string ToDelimitedCase(string value, char delimiter) { if (string.IsNullOrEmpty(value)) { return string.Empty; } List tokens = TokenizeForCase(value); if (tokens.Count == 0) { return string.Empty; } using PooledResource stringBuilderBuffer = Buffers.GetStringBuilder( value.Length, out StringBuilder stringBuilder ); bool previousWasWord = false; bool previousWasNumeric = false; bool previousHadUppercase = false; bool forceDelimiter = false; for (int i = 0; i < tokens.Count; ++i) { CaseToken token = tokens[i]; if (token.Kind == CaseTokenKind.Separator) { if (previousWasWord) { forceDelimiter = true; } continue; } string sanitized = SanitizeWord(token.Value, removeStripChars: true); if (string.IsNullOrEmpty(sanitized)) { continue; } bool isNumeric = true; bool hasLetter = false; for (int j = 0; j < sanitized.Length; ++j) { char c = sanitized[j]; if (!char.IsDigit(c)) { isNumeric = false; } if (char.IsLetter(c)) { hasLetter = true; } } bool startsWithDigit = char.IsDigit(sanitized[0]); bool tokenHasUppercase = token.HasUppercase; bool nextWordHasUppercase = false; for (int lookahead = i + 1; lookahead < tokens.Count; ++lookahead) { CaseToken lookaheadToken = tokens[lookahead]; if (lookaheadToken.Kind == CaseTokenKind.Separator) { continue; } string lookaheadSanitized = SanitizeWord( lookaheadToken.Value, removeStripChars: true ); if (string.IsNullOrEmpty(lookaheadSanitized)) { continue; } nextWordHasUppercase = lookaheadToken.HasUppercase; break; } bool allowDigitLetterContinuation = previousWasWord && !forceDelimiter && startsWithDigit && hasLetter && !tokenHasUppercase && !previousHadUppercase && !nextWordHasUppercase; bool allowNumericContinuation = previousWasWord && !forceDelimiter && isNumeric && !previousWasNumeric && !previousHadUppercase && !nextWordHasUppercase; if (!allowDigitLetterContinuation && !allowNumericContinuation && previousWasWord) { if (stringBuilder.Length > 0 && stringBuilder[^1] != delimiter) { _ = stringBuilder.Append(delimiter); } } _ = stringBuilder.Append(sanitized.ToLowerInvariant()); previousWasWord = true; previousWasNumeric = isNumeric; previousHadUppercase = tokenHasUppercase; forceDelimiter = false; } return stringBuilder.ToString(); } private static bool StartsWithLowercaseWord(string value) { if (string.IsNullOrEmpty(value)) { return false; } for (int i = 0; i < value.Length; ++i) { char c = value[i]; if (CharsToStrip.Contains(c)) { continue; } if (WordSeparators.Contains(c) || char.IsWhiteSpace(c)) { continue; } if (char.IsLetter(c)) { return char.IsLower(c); } if (char.IsDigit(c)) { return false; } } return false; } private static void AppendTitleCasedWord(StringBuilder builder, string word) { if (string.IsNullOrEmpty(word)) { return; } bool firstLetterHandled = false; for (int i = 0; i < word.Length; ++i) { char c = word[i]; if (!firstLetterHandled && char.IsLetter(c)) { _ = builder.Append(char.ToUpperInvariant(c)); firstLetterHandled = true; } else if (!firstLetterHandled && char.IsDigit(c)) { _ = builder.Append(c); } else if (!firstLetterHandled) { _ = builder.Append(c); } else if (char.IsLetter(c)) { _ = builder.Append(char.ToLowerInvariant(c)); } else { _ = builder.Append(c); } } } private static void AppendLowerInvariant(StringBuilder builder, string value) { if (string.IsNullOrEmpty(value)) { return; } for (int i = 0; i < value.Length; ++i) { char c = value[i]; _ = builder.Append(char.IsLetter(c) ? char.ToLowerInvariant(c) : c); } } private static bool IsAllUppercaseLetters(string value) { if (string.IsNullOrEmpty(value)) { return false; } bool foundLetter = false; for (int i = 0; i < value.Length; ++i) { char c = value[i]; if (!char.IsLetter(c)) { continue; } foundLetter = true; if (!char.IsUpper(c)) { return false; } } return foundLetter; } private static string CollapseSpaces(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } using PooledResource builderResource = Buffers.GetStringBuilder( value.Length, out StringBuilder builder ); bool previousWasSpace = false; for (int i = 0; i < value.Length; ++i) { char c = value[i]; if (char.IsWhiteSpace(c)) { if (!previousWasSpace) { _ = builder.Append(' '); previousWasSpace = true; } } else { _ = builder.Append(c); previousWasSpace = false; } } return builder.ToString().Trim(); } private static string ToTitleCaseInternal(string value, bool preserveSeparators) { if (string.IsNullOrEmpty(value)) { return string.Empty; } List tokens = TokenizeForCase(value); if (tokens.Count == 0) { return string.Empty; } bool startsWithLowerWord = StartsWithLowercaseWord(value); using PooledResource stringBuilderBuffer = Buffers.GetStringBuilder( value.Length, out StringBuilder stringBuilder ); CaseTokenKind? previousTokenKind = null; int previousWordLength = 0; for (int i = 0; i < tokens.Count; ++i) { CaseToken token = tokens[i]; if (token.Kind == CaseTokenKind.Separator) { if (preserveSeparators) { _ = stringBuilder.Append(token.Value); } else if (stringBuilder.Length > 0 && stringBuilder[^1] != ' ') { _ = stringBuilder.Append(' '); } previousTokenKind = CaseTokenKind.Separator; previousWordLength = 0; continue; } string sanitized = SanitizeWord(token.Value, removeStripChars: false); if (string.IsNullOrEmpty(sanitized)) { previousTokenKind = CaseTokenKind.Word; continue; } bool implicitBoundary = previousTokenKind == CaseTokenKind.Word; bool treatAsContinuation = implicitBoundary && startsWithLowerWord && previousWordLength <= 1 && IsAllUppercaseLetters(sanitized); if (implicitBoundary && !treatAsContinuation) { bool shouldInsertSpace = !startsWithLowerWord; if (preserveSeparators) { if (shouldInsertSpace) { _ = stringBuilder.Append(' '); } } else if ( shouldInsertSpace && stringBuilder.Length > 0 && stringBuilder[^1] != ' ' ) { _ = stringBuilder.Append(' '); } } if (treatAsContinuation) { AppendLowerInvariant(stringBuilder, sanitized); previousWordLength += sanitized.Length; } else { AppendTitleCasedWord(stringBuilder, sanitized); previousWordLength = sanitized.Length; } previousTokenKind = CaseTokenKind.Word; } if (!preserveSeparators) { return CollapseSpaces(stringBuilder.ToString()); } return stringBuilder.ToString(); } public static string ToPascalCase(this string value, string separator = "") { if (string.IsNullOrEmpty(value)) { return string.Empty; } List tokens = TokenizeForCase(value); if (tokens.Count == 0) { return string.Empty; } using PooledResource stringBuilderBuffer = Buffers.GetStringBuilder( value.Length, out StringBuilder stringBuilder ); bool isFirstWord = true; foreach (CaseToken token in tokens) { if (token.Kind != CaseTokenKind.Word) { continue; } string sanitized = SanitizeWord(token.Value, removeStripChars: true); if (string.IsNullOrEmpty(sanitized)) { continue; } if (!isFirstWord && !string.IsNullOrEmpty(separator)) { _ = stringBuilder.Append(separator); } AppendWordWithCasing(stringBuilder, sanitized, uppercaseFirstLetter: true); isFirstWord = false; } return stringBuilder.ToString(); } public static bool NeedsLowerInvariantConversion(this string input) { if (string.IsNullOrWhiteSpace(input)) { return false; } foreach (char inputCharacter in input) { if (char.ToLowerInvariant(inputCharacter) != inputCharacter) { return true; } } return false; } public static bool NeedsTrim(this string input) { if (string.IsNullOrEmpty(input)) { return false; } return char.IsWhiteSpace(input[0]) || char.IsWhiteSpace(input[^1]); } public static string Truncate(this string input, int maxLength, string ellipsis = "...") { if (string.IsNullOrEmpty(input) || maxLength < 0) { return input; } if (input.Length <= maxLength) { return input; } if (string.IsNullOrEmpty(ellipsis)) { return input.Substring(0, maxLength); } int truncateLength = Math.Max(0, maxLength - ellipsis.Length); return input.Substring(0, truncateLength) + ellipsis; } private static bool IsAlreadyCamelCase(string value) { if (string.IsNullOrEmpty(value)) { return false; } bool hasSeenLetter = false; for (int i = 0; i < value.Length; ++i) { char c = value[i]; if (WordSeparators.Contains(c) || char.IsWhiteSpace(c) || CharsToStrip.Contains(c)) { return false; } if (char.IsLetter(c)) { if (!hasSeenLetter) { if (!char.IsLower(c)) { return false; } hasSeenLetter = true; } if (char.IsUpper(c)) { int nextIndex = i + 1; if (nextIndex < value.Length && char.IsUpper(value[nextIndex])) { return false; } } } else if (!char.IsDigit(c)) { return false; } } return true; } public static string ToCamelCase(this string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } if (IsAlreadyCamelCase(value)) { return value; } string pascalCase = value.ToPascalCase(); if (pascalCase.Length == 0) { return string.Empty; } if (pascalCase.Length == 1) { return char.ToLowerInvariant(pascalCase[0]).ToString(); } pascalCase = RemoveCombiningDotAboveIfPresent(pascalCase); using PooledResource stringBuilderBuffer = Buffers.GetStringBuilder( value.Length, out StringBuilder stringBuilder ); _ = stringBuilder.Append(char.ToLowerInvariant(pascalCase[0])); for (int i = 1; i < pascalCase.Length; ++i) { _ = stringBuilder.Append(pascalCase[i]); } return stringBuilder.ToString(); } public static string ToSnakeCase(this string value) { return ToDelimitedCase(value, '_'); } public static string ToKebabCase(this string value) { return ToDelimitedCase(value, '-'); } public static string ToTitleCase(this string value, bool preserveSeparators = true) { return ToTitleCaseInternal(value, preserveSeparators); } public static bool ContainsIgnoreCase(this string input, string value) { if (input == null || value == null) { return false; } return input.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0; } public static bool EqualsIgnoreCase(this string input, string value) { return string.Equals(input, value, StringComparison.OrdinalIgnoreCase); } public static string Reverse(this string input) { if (input == null || input.Length <= 1) { return input; } int len = input.Length; return string.Create( len, input, static (span, src) => { for (int i = 0; i < span.Length; ++i) { span[i] = src[src.Length - 1 - i]; } } ); } public static string RemoveWhitespace(this string input) { if (string.IsNullOrEmpty(input)) { return input; } using PooledResource stringBuilderBuffer = Buffers.GetStringBuilder( input.Length, out StringBuilder stringBuilder ); foreach (char c in input) { if (!char.IsWhiteSpace(c)) { _ = stringBuilder.Append(c); } } return stringBuilder.ToString(); } public static int CountOccurrences(this string input, char character) { if (string.IsNullOrEmpty(input)) { return 0; } int count = 0; foreach (char c in input) { if (c == character) { ++count; } } return count; } public static int CountOccurrences(this string input, string substring) { if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(substring)) { return 0; } int count = 0; int index = 0; while ((index = input.IndexOf(substring, index, StringComparison.Ordinal)) != -1) { ++count; index += substring.Length; } return count; } public static bool IsNumeric(this string input) { if (string.IsNullOrEmpty(input)) { return false; } foreach (char c in input) { if (!char.IsDigit(c)) { return false; } } return true; } public static bool IsAlphabetic(this string input) { if (string.IsNullOrEmpty(input)) { return false; } foreach (char c in input) { if (!char.IsLetter(c)) { return false; } } return true; } public static bool IsAlphanumeric(this string input) { if (string.IsNullOrEmpty(input)) { return false; } foreach (char c in input) { if (!char.IsLetterOrDigit(c)) { return false; } } return true; } public static string ToBase64(this string input) { if (string.IsNullOrEmpty(input)) { return string.Empty; } byte[] bytes = Encoding.UTF8.GetBytes(input); return Convert.ToBase64String(bytes); } public static string FromBase64(this string input) { if (string.IsNullOrEmpty(input)) { return string.Empty; } if (TryDecodeBase64Utf8(input, out string decoded)) { return decoded; } return string.Empty; } private static bool IsLikelyBase64(string s) { int len = s.Length; if (len == 0 || (len & 3) != 0) { return false; } // Count '=' padding at end (0..2), and ensure it only appears at the end int padding = 0; if (s[len - 1] == '=') { padding = 1; if (s[len - 2] == '=') { padding = 2; } } int effectiveLen = len - padding; for (int i = 0; i < effectiveLen; ++i) { char c = s[i]; bool isAlphaUpper = c is >= 'A' and <= 'Z'; bool isAlphaLower = c is >= 'a' and <= 'z'; bool isDigit = c is >= '0' and <= '9'; bool isPlusSlash = c == '+' || c == '/'; if (!(isAlphaUpper || isAlphaLower || isDigit || isPlusSlash)) { return false; } } // Ensure no '=' appears before the padding region for (int i = 0; i < effectiveLen; ++i) { if (s[i] == '=') { return false; } } // Basic checks passed return true; } private static bool TryDecodeBase64Utf8(string s, out string result) { result = string.Empty; if (!IsLikelyBase64(s)) { return false; } int len = s.Length; int padding = 0; if (len > 0 && s[len - 1] == '=') { padding = 1; if (len > 1 && s[len - 2] == '=') { padding = 2; } } int outputLen = (len >> 2) * 3 - padding; if (outputLen < 0) { return false; } using PooledArray lease = SystemArrayPool.Get(outputLen, out byte[] buffer); int k = 0; for (int i = 0; i < len; i += 4) { int v0 = Base64Map(s[i]); int v1 = Base64Map(s[i + 1]); char c2 = s[i + 2]; char c3 = s[i + 3]; int v2 = c2 == '=' ? 0 : Base64Map(c2); int v3 = c3 == '=' ? 0 : Base64Map(c3); if (v0 < 0 || v1 < 0 || (c2 != '=' && v2 < 0) || (c3 != '=' && v3 < 0)) { return false; } if (k < outputLen) { buffer[k++] = (byte)((v0 << 2) | (v1 >> 4)); } if (k < outputLen) { buffer[k++] = (byte)((v1 << 4) | (v2 >> 2)); } if (k < outputLen) { buffer[k++] = (byte)((v2 << 6) | v3); } } result = Encoding.UTF8.GetString(buffer, 0, outputLen); return true; } private static int Base64Map(char c) { if (c is >= 'A' and <= 'Z') { return c - 'A'; } if (c is >= 'a' and <= 'z') { return c - 'a' + 26; } if (c is >= '0' and <= '9') { return c - '0' + 52; } if (c == '+') { return 62; } if (c == '/') { return 63; } return -1; } public static string Repeat(this string input, int count) { if (string.IsNullOrEmpty(input) || count <= 0) { return string.Empty; } if (count == 1) { return input; } int estimated = 0; if (count > 1 && input.Length > 0) { int maxMultiplier = int.MaxValue / input.Length; if (count <= maxMultiplier) { estimated = input.Length * count; } // else leave estimated = 0 to let the builder grow dynamically } using PooledResource stringBuilderBuffer = Buffers.GetStringBuilder( estimated, out StringBuilder stringBuilder ); for (int i = 0; i < count; ++i) { _ = stringBuilder.Append(input); } return stringBuilder.ToString(); } public static string[] SplitCamelCase(this string input) { if (string.IsNullOrEmpty(input)) { return Array.Empty(); } using PooledResource> listBuffer = Buffers.List.Get( out List words ); using PooledResource stringBuilderBuffer = Buffers.GetStringBuilder( input.Length, out StringBuilder currentWord ); for (int i = 0; i < input.Length; ++i) { char current = input[i]; if (WordSeparators.Contains(current)) { if (currentWord.Length > 0) { words.Add(currentWord.ToString()); currentWord.Clear(); } continue; } if (char.IsUpper(current) && i > 0) { char previous = input[i - 1]; if (!char.IsUpper(previous) && !WordSeparators.Contains(previous)) { if (currentWord.Length > 0) { words.Add(currentWord.ToString()); currentWord.Clear(); } } else if (i + 1 < input.Length && char.IsLower(input[i + 1])) { if (currentWord.Length > 0) { words.Add(currentWord.ToString()); currentWord.Clear(); } } } _ = currentWord.Append(current); } if (currentWord.Length > 0) { words.Add(currentWord.ToString()); } return words.ToArray(); } public static string ReplaceFirst(this string input, string oldValue, string newValue) { if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(oldValue)) { return input; } int index = input.IndexOf(oldValue, StringComparison.Ordinal); if (index < 0) { return input; } int oldLen = oldValue.Length; int newLen = input.Length - oldLen + (newValue?.Length ?? 0); return string.Create( newLen, (input, index, oldLen, newValue), static (dst, state) => { state.input.AsSpan(0, state.index).CopyTo(dst); int pos = state.index; if (state.newValue != null) { state.newValue.AsSpan().CopyTo(dst.Slice(pos)); pos += state.newValue.Length; } state.input.AsSpan(state.index + state.oldLen).CopyTo(dst.Slice(pos)); } ); } public static string ReplaceLast(this string input, string oldValue, string newValue) { if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(oldValue)) { return input; } int index = input.LastIndexOf(oldValue, StringComparison.Ordinal); if (index < 0) { return input; } int oldLen = oldValue.Length; int newLen = input.Length - oldLen + (newValue?.Length ?? 0); return string.Create( newLen, (input, index, oldLen, newValue), static (dst, state) => { state.input.AsSpan(0, state.index).CopyTo(dst); int pos = state.index; if (state.newValue != null) { state.newValue.AsSpan().CopyTo(dst.Slice(pos)); pos += state.newValue.Length; } state.input.AsSpan(state.index + state.oldLen).CopyTo(dst.Slice(pos)); } ); } private static string RemoveCombiningDotAboveIfPresent(string value) { if (string.IsNullOrEmpty(value)) { return value; } bool containsSpecialCharacter = false; for (int i = 0; i < value.Length; ++i) { char c = value[i]; if (c == CombiningDotAbove || c == CapitalIWithDot) { containsSpecialCharacter = true; break; } } if (!containsSpecialCharacter) { return value; } using PooledResource builderResource = Buffers.GetStringBuilder( value.Length, out StringBuilder builder ); for (int i = 0; i < value.Length; ++i) { char c = value[i]; if (c == CombiningDotAbove) { continue; } if (c == CapitalIWithDot) { _ = builder.Append(char.ToLowerInvariant('I')); continue; } _ = builder.Append(c); } return builder.ToString(); } public static string ToCase(this string value, StringCase stringCase) { switch (stringCase) { case StringCase.PascalCase: return value.ToPascalCase(); case StringCase.CamelCase: return value.ToCamelCase(); case StringCase.SnakeCase: return value.ToSnakeCase(); case StringCase.KebabCase: return value.ToKebabCase(); case StringCase.TitleCase: return value.ToTitleCase(preserveSeparators: false); case StringCase.LowerCase: return value == null ? string.Empty : RemoveCombiningDotAboveIfPresent(value.ToLowerInvariant()); case StringCase.UpperCase: return value?.ToUpperInvariant() ?? string.Empty; case StringCase.LowerInvariant: return value == null ? string.Empty : RemoveCombiningDotAboveIfPresent(value.ToLowerInvariant()); case StringCase.UpperInvariant: return value?.ToUpperInvariant() ?? string.Empty; #pragma warning disable CS0618 // Type or member is obsolete case StringCase.None: #pragma warning restore CS0618 // Type or member is obsolete default: return value ?? string.Empty; } } } }