Add & improve string extensions and add tests

This commit is contained in:
Stone_Red
2025-11-25 19:19:32 +01:00
parent 7184b6b15c
commit d588b6049b
2 changed files with 276 additions and 112 deletions
+161
View File
@@ -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<NullReferenceException>(() => 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<NullReferenceException>(() => 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<NullReferenceException>(() => 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<NullReferenceException>(() => nullStr!.RemoveDiacritics());
}
}
+113 -110
View File
@@ -1,169 +1,172 @@
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
namespace CuteUtils.Misc;
/// <summary>
/// <see cref="string"/> Extensions
/// Provides extension methods for working with <see cref="string"/> values.
/// </summary>
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();
}
/// <summary>
/// Removes all invalid chars from the specified <see cref="string"/>
/// Converts the string into a valid file name by removing invalid characters
/// and optionally removing spaces. Diacritics (accents) are also removed.
/// </summary>
/// <param name="str"></param>
/// <param name="allowSpaces"></param>
/// <returns></returns>
/// <param name="str">The input string.</param>
/// <param name="allowSpaces">Whether spaces should be preserved.</param>
/// <returns>A sanitized string that is safe to use as a file name.</returns>
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;
}
/// <summary>
/// Removes all invalid chars from the specified <see cref="string"/>
/// Converts the string into a valid file system path segment by removing invalid characters
/// and optionally removing spaces. Diacritics (accents) are also removed.
/// </summary>
/// <param name="str"></param>
/// <param name="allowSpaces"></param>
/// <returns></returns>
/// <param name="str">The input string.</param>
/// <param name="allowSpaces">Whether spaces should be preserved.</param>
/// <returns>A sanitized string that is safe to use as a path segment.</returns>
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);
}
/// <summary>
/// Truncates a <see cref="string"/> to the specified length.
/// Returns a truncated version of the string with a maximum length.
/// </summary>
/// <param name="str"></param>
/// <param name="length"></param>
/// <returns></returns>
/// <param name="str">The input string.</param>
/// <param name="length">The maximum allowed length.</param>
/// <returns>
/// The truncated string if the input exceeds <paramref name="length"/>;
/// otherwise, the original string.
/// </returns>
public static string Truncate(this string str, int length)
{
if (str.Length > length && length > 0)
{
return str[..length];
return (length > 0 && str.Length > length) ? str[..length] : str;
}
/// <summary>
/// Returns a truncated version of the string with a maximum length,
/// optionally appending an ellipsis ("...") if the string is shortened.
/// </summary>
/// <param name="str">The input string.</param>
/// <param name="length">The maximum allowed length.</param>
/// <param name="ellipsis">Whether to append "..." when truncated.</param>
/// <returns>The truncated string, with optional ellipsis.</returns>
public static string Truncate(this string str, int length, bool ellipsis)
{
if (length <= 0 || str.Length <= length)
{
return str;
}
/// <summary>
/// Truncates a <see cref="string"/> to the specified length.
/// </summary>
/// <param name="str"></param>
/// <param name="length"></param>
/// <param name="ellipsis"></param>
/// <returns></returns>
public static string Truncate(this string str, int length, bool ellipsis)
{
if (str.Length > length && length > 0)
{
if (ellipsis && length > 3)
{
return $"{str[..(length - 3)]}...";
}
else
{
return str[..length];
}
}
return str;
}
/// <summary>
/// Uses the correct newline <see cref="string"/> defined for this environment.
/// Normalizes newline characters in the string to the current environment's newline format.
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
/// <param name="str">The input string.</param>
/// <returns>
/// A string where all newline sequences are converted to
/// <see cref="Environment.NewLine"/>.
/// </returns>
/// <remarks>
/// This method safely normalizes mixed newline styles (CR, LF, CRLF).
/// </remarks>
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);
}
/// <summary>
/// Removes all white spaces from the specified <see cref="string"/>
/// Removes all whitespace characters from the string.
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
/// <param name="str">The input string.</param>
/// <returns>The string with all whitespace removed.</returns>
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());
}
/// <summary>
/// Reverses the specified <see cref="string"/>
/// Returns a new string with the characters reversed.
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
/// <param name="str">The input string.</param>
/// <returns>The reversed string.</returns>
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);
}
/// <summary>
/// Replaces all occurrences of a substring with another string,
/// using a case-insensitive comparison.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="search">The substring to search for.</param>
/// <param name="replacement">The replacement text.</param>
/// <returns>The modified string.</returns>
public static string ReplaceCaseInsensitive(this string input, string search, string replacement)
{
return Regex.Replace(input, Regex.Escape(search), replacement.Replace("$", "$$"), RegexOptions.IgnoreCase);
}
/// <summary>
/// Removes diacritic marks (accents) from characters in the string.
/// </summary>
/// <param name="str">The input string.</param>
/// <returns>The string with diacritics removed.</returns>
/// <remarks>
/// This is useful for normalization and for generating file-safe names.
/// </remarks>
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);
}
}