mirror of
https://github.com/Stone-Red-Code/CuteUtils.git
synced 2026-09-04 00:46:11 +02:00
Add & improve string extensions and add tests
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 str;
|
||||
return (length > 0 && str.Length > length) ? str[..length] : str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates a <see cref="string"/> to the specified length.
|
||||
/// Returns a truncated version of the string with a maximum length,
|
||||
/// optionally appending an ellipsis ("...") if the string is shortened.
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="length"></param>
|
||||
/// <param name="ellipsis"></param>
|
||||
/// <returns></returns>
|
||||
/// <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 (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];
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user