diff --git a/src/CuteUtils.Tests/Misc/StringExtTests.cs b/src/CuteUtils.Tests/Misc/StringExtTests.cs new file mode 100644 index 0000000..b901a2f --- /dev/null +++ b/src/CuteUtils.Tests/Misc/StringExtTests.cs @@ -0,0 +1,161 @@ +using CuteUtils.Misc; + +namespace CuteUtils.Tests.Misc; + +[TestClass] +public class StringExtTests_Coverage +{ + [TestMethod] + public void ToFileName_EmptyAndNull_ReturnsEmptyOrThrows() + { + Assert.AreEqual("", "".ToFileName()); + Assert.AreEqual("", "".ToFileName(allowSpaces: false)); + + string? nullStr = null; + _ = Assert.ThrowsExactly(() => nullStr!.ToFileName()); + } + + [TestMethod] + public void ToFileName_NormalizesUnicodeAccents() + { + string input = "Ångström Îñţérñåţîöñåļ"; + string output = input.ToFileName(); + + Assert.IsFalse(output.Any(c => c > 127)); // no extended unicode + Assert.Contains("An", output); // Å -> A + n (typical decompose) + } + + [TestMethod] + public void ToFileName_LongString_TrimsProperly() + { + string longInput = new('x', 500); + string result = longInput.ToFileName(); + + Assert.IsLessThanOrEqualTo(255, result.Length, "File name should be trimmed to FS-safe length"); + } + + [TestMethod] + public void ToPath_EmptyAndNull_ReturnsEmptyOrThrows() + { + Assert.AreEqual("", "".ToPath()); + + string? nullStr = null; + _ = Assert.ThrowsExactly(() => nullStr!.ToPath()); + } + + [TestMethod] + public void ToPath_RemovesBothFileAndPathInvalidChars() + { + string input = "t<>e\"s|t/dir\\name:*?"; + string output = input.ToPath(); + + foreach (char c in Path.GetInvalidPathChars()) + { + Assert.DoesNotContain(c, output); + } + } + + [TestMethod] + public void Truncate_HandlesNull_Throws() + { + string? nullStr = null; + _ = Assert.ThrowsExactly(() => nullStr!.Truncate(5)); + } + + [TestMethod] + public void Truncate_LongString_Performance() + { + string longStr = new('x', 10_000); + string result = longStr.Truncate(100); + + Assert.AreEqual(100, result.Length); + } + + [TestMethod] + public void CorrectNewLine_EmptyString_ReturnsEmpty() + { + Assert.AreEqual("", "".CorrectNewLine()); + } + + [TestMethod] + public void CorrectNewLine_Mixed_Newlines_Normalizes() + { + string mixed = "a\r\nb\nc\rd"; + + string result = mixed.CorrectNewLine(); + + if (Environment.OSVersion.Platform == PlatformID.Unix) + { + Assert.AreEqual("a\nb\nc\nd", result); + } + else + { + Assert.AreEqual("a\r\nb\r\nc\r\nd", result); + } + } + + [TestMethod] + public void RemoveWhitespaces_UnicodeSpaces() + { + string input = "a\u2003b\u3000c"; // em-space, ideographic space + Assert.AreEqual("abc", input.RemoveWhitespaces()); + } + + [TestMethod] + public void Reverse_EmptyAndSingleCharacter() + { + Assert.AreEqual("", "".Reverse()); + Assert.AreEqual("a", "a".Reverse()); + } + + [TestMethod] + public void Reverse_UnicodeCombiningCharacters() + { + string input = "e\u0301"; // é composed as e + diacritic + string result = input.Reverse(); + + Assert.AreEqual("\u0301e", result); + } + + [TestMethod] + public void ReplaceCaseInsensitive_UnicodeCase() + { + string input = "Straße"; // Eszett + string replaced = input.ReplaceCaseInsensitive("ß", "ss"); + + Assert.AreEqual("Strasse", replaced); + } + + [TestMethod] + public void ReplaceCaseInsensitive_ReplacesOverlappingCorrectly() + { + string input = "aaaa"; + string replaced = input.ReplaceCaseInsensitive("aa", "b"); + + Assert.AreEqual("bb", replaced); // correct non-overlapping behavior + } + + [TestMethod] + public void RemoveDiacritics_RemovesAccents_PreservesBaseCharacters() + { + // composed character + string caf = "café"; + Assert.AreEqual("cafe", caf.RemoveDiacritics()); + + // another composed example (Å -> A or Ang depending on normalization) + string ang = "Ångström"; + Assert.AreEqual("Angstrom", ang.RemoveDiacritics()); + + // decomposed form: e + combining acute accent + string decomposed = "e\u0301"; // e + ◌́ + Assert.AreEqual("e", decomposed.RemoveDiacritics()); + + // empty string stays empty + Assert.AreEqual(string.Empty, string.Empty.RemoveDiacritics()); + + // null behavior: current implementation will throw NullReferenceException + string? nullStr = null; + _ = Assert.ThrowsExactly(() => nullStr!.RemoveDiacritics()); + } + +} diff --git a/src/CuteUtils/Misc/StringExtentions.cs b/src/CuteUtils/Misc/StringExtentions.cs index 1c9d0dc..cec23b7 100644 --- a/src/CuteUtils/Misc/StringExtentions.cs +++ b/src/CuteUtils/Misc/StringExtentions.cs @@ -1,169 +1,172 @@ using System.Globalization; using System.Text; +using System.Text.RegularExpressions; namespace CuteUtils.Misc; /// -/// Extensions +/// Provides extension methods for working with values. /// public static class StringExt { + internal static string Sanitize(string str, char[] invalidChars, bool allowSpaces) + { + if (!allowSpaces) + { + str = str.Replace(" ", string.Empty); + } + + foreach (char c in invalidChars) + { + str = str.Replace(c.ToString(), string.Empty); + } + + return str.RemoveDiacritics(); + } + /// - /// Removes all invalid chars from the specified + /// Converts the string into a valid file name by removing invalid characters + /// and optionally removing spaces. Diacritics (accents) are also removed. /// - /// - /// - /// + /// The input string. + /// Whether spaces should be preserved. + /// A sanitized string that is safe to use as a file name. public static string ToFileName(this string str, bool allowSpaces = false) { - char[] invalidChars = Path.GetInvalidFileNameChars(); + string path = Sanitize(str, Path.GetInvalidFileNameChars(), allowSpaces); - if (!allowSpaces) + if (path.Length > 255) { - str = str.Replace(" ", string.Empty); + path = path[..255]; } - foreach (char item in invalidChars) - { - str = str.Replace(item.ToString(), string.Empty); - } - - string normalizedString = str.Normalize(NormalizationForm.FormD); - StringBuilder stringBuilder = new StringBuilder(); - - foreach (char c in normalizedString) - { - UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); - if (unicodeCategory != UnicodeCategory.NonSpacingMark) - { - _ = stringBuilder.Append(c); - } - } - - return stringBuilder.ToString().Normalize(NormalizationForm.FormC); + return path; } /// - /// Removes all invalid chars from the specified + /// Converts the string into a valid file system path segment by removing invalid characters + /// and optionally removing spaces. Diacritics (accents) are also removed. /// - /// - /// - /// + /// The input string. + /// Whether spaces should be preserved. + /// A sanitized string that is safe to use as a path segment. public static string ToPath(this string str, bool allowSpaces = false) { - char[] invalidChars = Path.GetInvalidPathChars(); - - if (!allowSpaces) - { - str = str.Replace(" ", string.Empty); - } - - foreach (char item in invalidChars) - { - str = str.Replace(item.ToString(), string.Empty); - } - - string normalizedString = str.Normalize(NormalizationForm.FormD); - StringBuilder stringBuilder = new StringBuilder(); - - foreach (char c in normalizedString) - { - UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); - if (unicodeCategory != UnicodeCategory.NonSpacingMark) - { - _ = stringBuilder.Append(c); - } - } - - return stringBuilder.ToString().Normalize(NormalizationForm.FormC); + return Sanitize(str, Path.GetInvalidPathChars(), allowSpaces); } /// - /// Truncates a to the specified length. + /// Returns a truncated version of the string with a maximum length. /// - /// - /// - /// + /// The input string. + /// The maximum allowed length. + /// + /// The truncated string if the input exceeds ; + /// otherwise, the original string. + /// public static string Truncate(this string str, int length) { - if (str.Length > length && length > 0) - { - return str[..length]; - } - - return str; + return (length > 0 && str.Length > length) ? str[..length] : str; } /// - /// Truncates a to the specified length. + /// Returns a truncated version of the string with a maximum length, + /// optionally appending an ellipsis ("...") if the string is shortened. /// - /// - /// - /// - /// + /// The input string. + /// The maximum allowed length. + /// Whether to append "..." when truncated. + /// The truncated string, with optional ellipsis. public static string Truncate(this string str, int length, bool ellipsis) { - if (str.Length > length && length > 0) + if (length <= 0 || str.Length <= length) { - if (ellipsis && length > 3) - { - return $"{str[..(length - 3)]}..."; - } - else - { - return str[..length]; - } + return str; } - return str; + if (ellipsis && length > 3) + { + return $"{str[..(length - 3)]}..."; + } + + return str[..length]; } /// - /// Uses the correct newline defined for this environment. + /// Normalizes newline characters in the string to the current environment's newline format. /// - /// - /// + /// The input string. + /// + /// A string where all newline sequences are converted to + /// . + /// + /// + /// This method safely normalizes mixed newline styles (CR, LF, CRLF). + /// public static string CorrectNewLine(this string str) { - if (Environment.OSVersion.Platform == PlatformID.Unix) - { - str = str.Replace("\r\n", "\n"); - } - else - { - str = str.Replace("\n", "\r\n"); //Ik that this can produce wrong results - } - - return str; + return str + .Replace("\r\n", "\n") + .Replace("\r", "\n") + .Replace("\n", Environment.NewLine); } /// - /// Removes all white spaces from the specified + /// Removes all whitespace characters from the string. /// - /// - /// + /// The input string. + /// The string with all whitespace removed. public static string RemoveWhitespaces(this string str) { - StringBuilder result = new StringBuilder(); - foreach (char c in str) - { - if (!char.IsWhiteSpace(c)) - { - _ = result.Append(c); - } - } - return result.ToString(); + return new string(str.Where(c => !char.IsWhiteSpace(c)).ToArray()); } /// - /// Reverses the specified + /// Returns a new string with the characters reversed. /// - /// - /// + /// The input string. + /// The reversed string. public static string Reverse(this string str) { - char[] array = str.ToCharArray(); - Array.Reverse(array); - return new string(array); + char[] arr = str.ToCharArray(); + Array.Reverse(arr); + return new string(arr); } -} \ No newline at end of file + + /// + /// Replaces all occurrences of a substring with another string, + /// using a case-insensitive comparison. + /// + /// The input string. + /// The substring to search for. + /// The replacement text. + /// The modified string. + public static string ReplaceCaseInsensitive(this string input, string search, string replacement) + { + return Regex.Replace(input, Regex.Escape(search), replacement.Replace("$", "$$"), RegexOptions.IgnoreCase); + } + + /// + /// Removes diacritic marks (accents) from characters in the string. + /// + /// The input string. + /// The string with diacritics removed. + /// + /// This is useful for normalization and for generating file-safe names. + /// + public static string RemoveDiacritics(this string str) + { + string normalized = str.Normalize(NormalizationForm.FormD); + StringBuilder sb = new StringBuilder(normalized.Length); + + foreach (char c in normalized) + { + if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark) + { + _ = sb.Append(c); + } + } + + return sb.ToString().Normalize(NormalizationForm.FormC); + } +}