From 981ec07ce11c14a31e451f733ec0aa67aa3ed5f4 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:17:31 +0100 Subject: [PATCH 1/5] Add XML helper --- src/CuteUtils.Tests/Misc/XmlHelperTests.cs | 69 ++++++++++++++++ src/CuteUtils/Misc/XmlHelper.cs | 94 ++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 src/CuteUtils.Tests/Misc/XmlHelperTests.cs create mode 100644 src/CuteUtils/Misc/XmlHelper.cs diff --git a/src/CuteUtils.Tests/Misc/XmlHelperTests.cs b/src/CuteUtils.Tests/Misc/XmlHelperTests.cs new file mode 100644 index 0000000..0b018eb --- /dev/null +++ b/src/CuteUtils.Tests/Misc/XmlHelperTests.cs @@ -0,0 +1,69 @@ +using CuteUtils.Misc; + +namespace CuteUtils.Tests.Misc; + +[TestClass] +public class XmlHelperTests +{ + public class TestModel + { + public string? Name { get; set; } + public int Age { get; set; } + public NestedModel? Child { get; set; } + } + + public class NestedModel + { + public string? Value { get; set; } + } + + [TestMethod] + public void SerializeDeserialize_RoundTrip_RetainsData() + { + TestModel model = new TestModel + { + Name = "Alice", + Age = 30, + Child = new NestedModel { Value = "Inner" } + }; + + string xml = XmlHelper.Serialize(model); + Assert.IsFalse(string.IsNullOrWhiteSpace(xml), "Serialized XML should not be empty."); + Assert.Contains("Alice", xml, "XML should contain the Name value."); + Assert.Contains("30", xml, "XML should contain the Age value."); + Assert.Contains("Inner", xml, "XML should contain the Nested Value."); + + TestModel? deserialized = XmlHelper.Deserialize(xml); + Assert.IsNotNull(deserialized, "Deserialized object should not be null."); + Assert.AreEqual(model.Name, deserialized!.Name); + Assert.AreEqual(model.Age, deserialized.Age); + Assert.AreEqual(model.Child?.Value, deserialized.Child?.Value); + } + + [TestMethod] + public void Serialize_OmitsNamespaceDeclarations() + { + TestModel model = new TestModel { Name = "Bob", Age = 25 }; + string xml = XmlHelper.Serialize(model); + + // Expect no namespace attributes like xmlns or common schema prefixes + Assert.DoesNotContain("xmlns", xml, "XML should not contain namespace declarations."); + Assert.DoesNotContain("xsi:", xml, "XML should not contain xsi prefix."); + Assert.DoesNotContain("xsd:", xml, "XML should not contain xsd prefix."); + } + + [TestMethod] + public void Deserialize_NullXml_ThrowsArgumentNullException() + { + _ = Assert.ThrowsExactly(() => XmlHelper.Deserialize(null!)); + } + + [TestMethod] + public void GetXmlSchema_ReturnsSchemaContainingTypeName() + { + string schema = XmlHelper.GetXmlSchema(); + Assert.IsFalse(string.IsNullOrWhiteSpace(schema), "Schema should not be empty."); + Assert.IsGreaterThanOrEqualTo(0, schema.IndexOf("schema", StringComparison.OrdinalIgnoreCase), "Schema should contain 'schema' element."); + Assert.Contains("TestModel", schema, "Schema should reference the TestModel type."); + } +} \ No newline at end of file diff --git a/src/CuteUtils/Misc/XmlHelper.cs b/src/CuteUtils/Misc/XmlHelper.cs new file mode 100644 index 0000000..56fe364 --- /dev/null +++ b/src/CuteUtils/Misc/XmlHelper.cs @@ -0,0 +1,94 @@ +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; + +namespace CuteUtils.Misc; + +/// +/// Provides simple XML serialization and deserialization helpers and schema generation helpers. +/// +public static class XmlHelper +{ + /// + /// Deserializes an XML string into an instance of . + /// + /// The target reference type to deserialize into. Must be a class. + /// The XML string to deserialize. Must not be null. + /// Optional to control reader behavior. If null, default settings are used. + /// An instance of if deserialization succeeds; otherwise, null. + /// Thrown when is null. + /// May be thrown by when deserialization fails. + public static T? Deserialize(string xml, XmlReaderSettings? settings = null) where T : class + { + ArgumentNullException.ThrowIfNull(xml); + + XmlSerializer serializer = new XmlSerializer(typeof(T)); + + using StringReader stringReader = new StringReader(xml); + using XmlReader xmlReader = XmlReader.Create(stringReader, settings); + + return serializer.Deserialize(xmlReader) as T; + } + + /// + /// Serializes an object to its XML representation. + /// + /// The type of the object to serialize. + /// The object instance to serialize. Must not be null. + /// Optional to control writer behavior. If null, default settings are used. + /// Optional to control namespace declarations. If null, an empty namespace is used to omit declarations. + /// A string containing the XML representation of . + /// Thrown when is null. + /// May be thrown by when serialization fails. + public static string Serialize(T obj, XmlWriterSettings? settings = null, XmlSerializerNamespaces? xmlSerializerNamespaces = null) + { + ArgumentNullException.ThrowIfNull(obj); + + settings ??= new XmlWriterSettings(); + + if (xmlSerializerNamespaces == null) + { + xmlSerializerNamespaces = new XmlSerializerNamespaces(); + xmlSerializerNamespaces.Add(string.Empty, string.Empty); + } + + XmlSerializer xmlSerializer = new(typeof(T)); + + // Omit namespace declarations by adding an empty namespace + xmlSerializerNamespaces.Add(string.Empty, string.Empty); + + using StringWriter stringWriter = new StringWriter(); + using XmlWriter xmlWriter = XmlWriter.Create(stringWriter, settings); + + xmlSerializer.Serialize(xmlWriter, obj, xmlSerializerNamespaces); + return stringWriter.ToString(); + } + + /// + /// Generates XML schema definitions for the specified type . + /// + /// The type for which to generate XML schema. + /// Optional to control output formatting. If null, default settings are used. + /// A string containing one or more XML schema documents representing . + /// May be thrown when schema export fails. + public static string GetXmlSchema(XmlWriterSettings? settings = null) + { + settings ??= new XmlWriterSettings(); + + XmlSchemas schemas = []; + XmlSchemaExporter exporter = new XmlSchemaExporter(schemas); + XmlTypeMapping mapping = new XmlReflectionImporter().ImportTypeMapping(typeof(T)); + + exporter.ExportTypeMapping(mapping); + + using StringWriter stringWriter = new StringWriter(); + using XmlWriter xmlWriter = XmlWriter.Create(stringWriter, settings); + + foreach (XmlSchema schema in schemas) + { + schema.Write(xmlWriter); + } + + return stringWriter.ToString(); + } +} \ No newline at end of file From 864604b36c97bd5f12352fd79bab9db2e8eba715 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:18:12 +0100 Subject: [PATCH 2/5] Fix FluentMath namespace --- src/CuteUtils/FluentMath/TypeExtensions/DecimalFluent.cs | 2 +- src/CuteUtils/FluentMath/TypeExtensions/DoubleFluent.cs | 2 +- src/CuteUtils/FluentMath/TypeExtensions/Int16Fluent.cs | 2 +- src/CuteUtils/FluentMath/TypeExtensions/Int32Fluent.cs | 2 +- src/CuteUtils/FluentMath/TypeExtensions/SingleFluent.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/CuteUtils/FluentMath/TypeExtensions/DecimalFluent.cs b/src/CuteUtils/FluentMath/TypeExtensions/DecimalFluent.cs index 9b31457..e5e2e6a 100644 --- a/src/CuteUtils/FluentMath/TypeExtensions/DecimalFluent.cs +++ b/src/CuteUtils/FluentMath/TypeExtensions/DecimalFluent.cs @@ -1,4 +1,4 @@ -namespace CuteUtils.FluentMath.TypeExtentions; +namespace CuteUtils.FluentMath.TypeExtensions; /// /// DecimalFluent class diff --git a/src/CuteUtils/FluentMath/TypeExtensions/DoubleFluent.cs b/src/CuteUtils/FluentMath/TypeExtensions/DoubleFluent.cs index 4ac4a31..47c0dd3 100644 --- a/src/CuteUtils/FluentMath/TypeExtensions/DoubleFluent.cs +++ b/src/CuteUtils/FluentMath/TypeExtensions/DoubleFluent.cs @@ -1,4 +1,4 @@ -namespace CuteUtils.FluentMath.TypeExtentions; +namespace CuteUtils.FluentMath.TypeExtensions; /// /// DoubleFluent class diff --git a/src/CuteUtils/FluentMath/TypeExtensions/Int16Fluent.cs b/src/CuteUtils/FluentMath/TypeExtensions/Int16Fluent.cs index 36bd397..2a23042 100644 --- a/src/CuteUtils/FluentMath/TypeExtensions/Int16Fluent.cs +++ b/src/CuteUtils/FluentMath/TypeExtensions/Int16Fluent.cs @@ -1,4 +1,4 @@ -namespace CuteUtils.FluentMath.TypeExtentions; +namespace CuteUtils.FluentMath.TypeExtensions; /// /// IntegerFluent class diff --git a/src/CuteUtils/FluentMath/TypeExtensions/Int32Fluent.cs b/src/CuteUtils/FluentMath/TypeExtensions/Int32Fluent.cs index a540a76..a2a0037 100644 --- a/src/CuteUtils/FluentMath/TypeExtensions/Int32Fluent.cs +++ b/src/CuteUtils/FluentMath/TypeExtensions/Int32Fluent.cs @@ -1,4 +1,4 @@ -namespace CuteUtils.FluentMath.TypeExtentions; +namespace CuteUtils.FluentMath.TypeExtensions; /// /// IntegerFluent class diff --git a/src/CuteUtils/FluentMath/TypeExtensions/SingleFluent.cs b/src/CuteUtils/FluentMath/TypeExtensions/SingleFluent.cs index 5dd63b3..ce5c3af 100644 --- a/src/CuteUtils/FluentMath/TypeExtensions/SingleFluent.cs +++ b/src/CuteUtils/FluentMath/TypeExtensions/SingleFluent.cs @@ -1,4 +1,4 @@ -namespace CuteUtils.FluentMath.TypeExtentions; +namespace CuteUtils.FluentMath.TypeExtensions; /// /// FloatFluent class From 7184b6b15c4a352a98ea3f42917b82c68d7d506a Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:19:02 +0100 Subject: [PATCH 3/5] Improve wait and try utilities --- src/CuteUtils.Tests/Misc/WaitTests.cs | 14 ++++---- src/CuteUtils/Misc/Try.cs | 2 +- src/CuteUtils/Misc/WaitUntil.cs | 46 +++++++++++++++++---------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/CuteUtils.Tests/Misc/WaitTests.cs b/src/CuteUtils.Tests/Misc/WaitTests.cs index 634a7ee..5bbc4ad 100644 --- a/src/CuteUtils.Tests/Misc/WaitTests.cs +++ b/src/CuteUtils.Tests/Misc/WaitTests.cs @@ -36,7 +36,7 @@ public class WaitTests await Task.Delay(200); flag = true; }); - Wait.Until(() => flag, TimeSpan.FromMilliseconds(50), TimeSpan.FromSeconds(2)); + Wait.Until(() => flag, TimeSpan.FromMilliseconds(50)); Assert.IsTrue(flag); } @@ -87,7 +87,7 @@ public class WaitTests handler(42); }); } - int result = await Wait.WaitForEventAsync((Action>)subscribe, TimeSpan.FromSeconds(2)); + int result = await Wait.ForEventAsync((Action>)subscribe, TimeSpan.FromSeconds(2)); Assert.AreEqual(42, result); } @@ -100,7 +100,7 @@ public class WaitTests } _ = await Assert.ThrowsExactlyAsync(async () => { - _ = await Wait.WaitForEventAsync((Action>)subscribe, TimeSpan.FromMilliseconds(200)); + _ = await Wait.ForEventAsync((Action>)subscribe, TimeSpan.FromMilliseconds(200)); }); } @@ -115,7 +115,7 @@ public class WaitTests handler(); }); } - Wait.WaitForEvent(subscribe, TimeSpan.FromSeconds(2)); + Wait.ForEvent(subscribe, TimeSpan.FromSeconds(2)); } [TestMethod] @@ -127,7 +127,7 @@ public class WaitTests } _ = Assert.ThrowsExactly(() => { - Wait.WaitForEvent(subscribe, TimeSpan.FromMilliseconds(200)); + Wait.ForEvent(subscribe, TimeSpan.FromMilliseconds(200)); }); } @@ -142,7 +142,7 @@ public class WaitTests handler("hello"); }); } - string result = Wait.WaitForEvent((Action>)subscribe, TimeSpan.FromSeconds(2)); + string result = Wait.ForEvent((Action>)subscribe, TimeSpan.FromSeconds(2)); Assert.AreEqual("hello", result); } @@ -155,7 +155,7 @@ public class WaitTests } _ = Assert.ThrowsExactly(() => { - _ = Wait.WaitForEvent((Action>)subscribe, TimeSpan.FromMilliseconds(200)); + _ = Wait.ForEvent((Action>)subscribe, TimeSpan.FromMilliseconds(200)); }); } } \ No newline at end of file diff --git a/src/CuteUtils/Misc/Try.cs b/src/CuteUtils/Misc/Try.cs index bd41ed6..5656588 100644 --- a/src/CuteUtils/Misc/Try.cs +++ b/src/CuteUtils/Misc/Try.cs @@ -124,7 +124,7 @@ public static class Try catch (Exception ex) { lastException = ex; - System.Threading.Thread.Sleep(delay.Value); + Thread.Sleep(delay.Value); } } throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException); diff --git a/src/CuteUtils/Misc/WaitUntil.cs b/src/CuteUtils/Misc/WaitUntil.cs index 470fc06..4c84876 100644 --- a/src/CuteUtils/Misc/WaitUntil.cs +++ b/src/CuteUtils/Misc/WaitUntil.cs @@ -1,4 +1,6 @@ -namespace CuteUtils.Misc; +using System.Diagnostics; + +namespace CuteUtils.Misc; /// /// Provides utility methods for waiting on conditions or events with optional timeouts and intervals. @@ -16,15 +18,20 @@ public static class Wait { timeout ??= TimeSpan.MaxValue; interval ??= TimeSpan.FromMilliseconds(100); - DateTime endTime = DateTime.UtcNow.Add(timeout.Value); - while (!condition() && DateTime.UtcNow < endTime) + + Stopwatch sw = Stopwatch.StartNew(); + + while (sw.Elapsed < timeout.Value) { + if (condition()) + { + return; + } + await Task.Delay(interval.Value); } - if (!condition()) - { - throw new TimeoutException("The condition was not met within the specified timeout."); - } + + throw new TimeoutException("The condition was not met within the specified timeout."); } /// @@ -38,15 +45,20 @@ public static class Wait { timeout ??= TimeSpan.MaxValue; interval ??= TimeSpan.FromMilliseconds(100); - DateTime endTime = DateTime.UtcNow.Add(timeout.Value); - while (!condition() && DateTime.UtcNow < endTime) + + Stopwatch sw = Stopwatch.StartNew(); + + while (sw.Elapsed < timeout.Value) { - System.Threading.Thread.Sleep(interval.Value); - } - if (!condition()) - { - throw new TimeoutException("The condition was not met within the specified timeout."); + if (condition()) + { + return; + } + + Thread.Sleep(interval.Value); } + + throw new TimeoutException("The condition was not met within the specified timeout."); } /// @@ -88,7 +100,7 @@ public static class Wait /// The type of the event argument. /// A task that completes with the event argument when the event is raised. /// Thrown if the event is not raised within the timeout. - public static async Task WaitForEventAsync(Action> subscribe, TimeSpan? timeout = null) + public static async Task ForEventAsync(Action> subscribe, TimeSpan? timeout = null) { timeout ??= TimeSpan.MaxValue; @@ -121,7 +133,7 @@ public static class Wait /// An action that subscribes a handler to the event. /// The maximum time to wait. Defaults to infinite. /// Thrown if the event is not raised within the timeout. - public static void WaitForEvent(Action subscribe, TimeSpan? timeout = null) + public static void ForEvent(Action subscribe, TimeSpan? timeout = null) { timeout ??= TimeSpan.MaxValue; TaskCompletionSource tcs = new TaskCompletionSource(); @@ -152,7 +164,7 @@ public static class Wait /// The type of the event argument. /// The event argument when the event is raised. /// Thrown if the event is not raised within the timeout. - public static T WaitForEvent(Action> subscribe, TimeSpan? timeout = null) + public static T ForEvent(Action> subscribe, TimeSpan? timeout = null) { timeout ??= TimeSpan.MaxValue; TaskCompletionSource tcs = new TaskCompletionSource(); From d588b6049bd34773483fd3cd993462db429a4835 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:19:32 +0100 Subject: [PATCH 4/5] Add & improve string extensions and add tests --- src/CuteUtils.Tests/Misc/StringExtTests.cs | 161 +++++++++++++++ src/CuteUtils/Misc/StringExtentions.cs | 227 +++++++++++---------- 2 files changed, 276 insertions(+), 112 deletions(-) create mode 100644 src/CuteUtils.Tests/Misc/StringExtTests.cs 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); + } +} From b136dda50ba128217c82abcd7d37fbdb3603830c Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:19:41 +0100 Subject: [PATCH 5/5] Update project to .NET 10 --- src/CuteUtils.Tests/CuteUtils.Tests.csproj | 8 +- src/CuteUtils/CuteUtils.csproj | 4 +- src/CuteUtils/Misc/ConsoleExtentions.cs | 249 +++++++++++---------- 3 files changed, 138 insertions(+), 123 deletions(-) diff --git a/src/CuteUtils.Tests/CuteUtils.Tests.csproj b/src/CuteUtils.Tests/CuteUtils.Tests.csproj index e625376..aacb2fb 100644 --- a/src/CuteUtils.Tests/CuteUtils.Tests.csproj +++ b/src/CuteUtils.Tests/CuteUtils.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -14,9 +14,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/src/CuteUtils/CuteUtils.csproj b/src/CuteUtils/CuteUtils.csproj index 6411c64..cb504a6 100644 --- a/src/CuteUtils/CuteUtils.csproj +++ b/src/CuteUtils/CuteUtils.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable True @@ -29,7 +29,7 @@ - +