Merge pull request #7 from Stone-Red-Code/develop

Develop
This commit is contained in:
Stone_Red
2025-11-25 19:21:33 +01:00
committed by GitHub
15 changed files with 619 additions and 265 deletions
+4 -4
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@@ -14,9 +14,9 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="MSTest.TestAdapter" Version="3.8.3" /> <PackageReference Include="MSTest.TestAdapter" Version="4.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.8.3" /> <PackageReference Include="MSTest.TestFramework" Version="4.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+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());
}
}
+7 -7
View File
@@ -36,7 +36,7 @@ public class WaitTests
await Task.Delay(200); await Task.Delay(200);
flag = true; flag = true;
}); });
Wait.Until(() => flag, TimeSpan.FromMilliseconds(50), TimeSpan.FromSeconds(2)); Wait.Until(() => flag, TimeSpan.FromMilliseconds(50));
Assert.IsTrue(flag); Assert.IsTrue(flag);
} }
@@ -87,7 +87,7 @@ public class WaitTests
handler(42); handler(42);
}); });
} }
int result = await Wait.WaitForEventAsync((Action<Action<int>>)subscribe, TimeSpan.FromSeconds(2)); int result = await Wait.ForEventAsync((Action<Action<int>>)subscribe, TimeSpan.FromSeconds(2));
Assert.AreEqual(42, result); Assert.AreEqual(42, result);
} }
@@ -100,7 +100,7 @@ public class WaitTests
} }
_ = await Assert.ThrowsExactlyAsync<TimeoutException>(async () => _ = await Assert.ThrowsExactlyAsync<TimeoutException>(async () =>
{ {
_ = await Wait.WaitForEventAsync((Action<Action<int>>)subscribe, TimeSpan.FromMilliseconds(200)); _ = await Wait.ForEventAsync((Action<Action<int>>)subscribe, TimeSpan.FromMilliseconds(200));
}); });
} }
@@ -115,7 +115,7 @@ public class WaitTests
handler(); handler();
}); });
} }
Wait.WaitForEvent(subscribe, TimeSpan.FromSeconds(2)); Wait.ForEvent(subscribe, TimeSpan.FromSeconds(2));
} }
[TestMethod] [TestMethod]
@@ -127,7 +127,7 @@ public class WaitTests
} }
_ = Assert.ThrowsExactly<TimeoutException>(() => _ = Assert.ThrowsExactly<TimeoutException>(() =>
{ {
Wait.WaitForEvent(subscribe, TimeSpan.FromMilliseconds(200)); Wait.ForEvent(subscribe, TimeSpan.FromMilliseconds(200));
}); });
} }
@@ -142,7 +142,7 @@ public class WaitTests
handler("hello"); handler("hello");
}); });
} }
string result = Wait.WaitForEvent((Action<Action<string>>)subscribe, TimeSpan.FromSeconds(2)); string result = Wait.ForEvent((Action<Action<string>>)subscribe, TimeSpan.FromSeconds(2));
Assert.AreEqual("hello", result); Assert.AreEqual("hello", result);
} }
@@ -155,7 +155,7 @@ public class WaitTests
} }
_ = Assert.ThrowsExactly<TimeoutException>(() => _ = Assert.ThrowsExactly<TimeoutException>(() =>
{ {
_ = Wait.WaitForEvent((Action<Action<string>>)subscribe, TimeSpan.FromMilliseconds(200)); _ = Wait.ForEvent((Action<Action<string>>)subscribe, TimeSpan.FromMilliseconds(200));
}); });
} }
} }
@@ -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<TestModel>(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<ArgumentNullException>(() => XmlHelper.Deserialize<TestModel>(null!));
}
[TestMethod]
public void GetXmlSchema_ReturnsSchemaContainingTypeName()
{
string schema = XmlHelper.GetXmlSchema<TestModel>();
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.");
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild> <GeneratePackageOnBuild>True</GeneratePackageOnBuild>
@@ -29,7 +29,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Reactive.Linq" Version="6.0.1" /> <PackageReference Include="System.Reactive.Linq" Version="6.1.0" />
</ItemGroup> </ItemGroup>
<!-- <!--
@@ -1,4 +1,4 @@
namespace CuteUtils.FluentMath.TypeExtentions; namespace CuteUtils.FluentMath.TypeExtensions;
/// <summary> /// <summary>
/// DecimalFluent class /// DecimalFluent class
@@ -1,4 +1,4 @@
namespace CuteUtils.FluentMath.TypeExtentions; namespace CuteUtils.FluentMath.TypeExtensions;
/// <summary> /// <summary>
/// DoubleFluent class /// DoubleFluent class
@@ -1,4 +1,4 @@
namespace CuteUtils.FluentMath.TypeExtentions; namespace CuteUtils.FluentMath.TypeExtensions;
/// <summary> /// <summary>
/// IntegerFluent class /// IntegerFluent class
@@ -1,4 +1,4 @@
namespace CuteUtils.FluentMath.TypeExtentions; namespace CuteUtils.FluentMath.TypeExtensions;
/// <summary> /// <summary>
/// IntegerFluent class /// IntegerFluent class
@@ -1,4 +1,4 @@
namespace CuteUtils.FluentMath.TypeExtentions; namespace CuteUtils.FluentMath.TypeExtensions;
/// <summary> /// <summary>
/// FloatFluent class /// FloatFluent class
+15
View File
@@ -10,6 +10,8 @@ namespace CuteUtils.Misc;
/// </summary> /// </summary>
public static class ConsoleExt public static class ConsoleExt
{ {
extension(Console)
{
/// <summary> /// <summary>
/// Writes the specified value to the console with the specified color. /// Writes the specified value to the console with the specified color.
/// </summary> /// </summary>
@@ -26,6 +28,12 @@ public static class ConsoleExt
} }
} }
/// <inheritdoc cref="Write(object, ConsoleColor)"/>
public static void WriteColor(object value, ConsoleColor color)
{
Write(value, color);
}
/// <summary> /// <summary>
/// Writes the specified value to the console with the specified color and appends a new line. /// Writes the specified value to the console with the specified color and appends a new line.
/// </summary> /// </summary>
@@ -42,6 +50,12 @@ public static class ConsoleExt
} }
} }
/// <inheritdoc cref="WriteLine(object, ConsoleColor)"/>
public static void WriteLineColor(object value, ConsoleColor color)
{
WriteLine(value, color);
}
/// <summary> /// <summary>
/// Reads the next line of characters from the standard input stream and tries to convert it to the specified type. /// Reads the next line of characters from the standard input stream and tries to convert it to the specified type.
/// </summary> /// </summary>
@@ -144,4 +158,5 @@ public static class ConsoleExt
Console.WriteLine(message); Console.WriteLine(message);
_ = Console.ReadKey(true); _ = Console.ReadKey(true);
} }
}
} }
+113 -110
View File
@@ -1,169 +1,172 @@
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
namespace CuteUtils.Misc; namespace CuteUtils.Misc;
/// <summary> /// <summary>
/// <see cref="string"/> Extensions /// Provides extension methods for working with <see cref="string"/> values.
/// </summary> /// </summary>
public static class StringExt 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> /// <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> /// </summary>
/// <param name="str"></param> /// <param name="str">The input string.</param>
/// <param name="allowSpaces"></param> /// <param name="allowSpaces">Whether spaces should be preserved.</param>
/// <returns></returns> /// <returns>A sanitized string that is safe to use as a file name.</returns>
public static string ToFileName(this string str, bool allowSpaces = false) 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) return path;
{
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);
} }
/// <summary> /// <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> /// </summary>
/// <param name="str"></param> /// <param name="str">The input string.</param>
/// <param name="allowSpaces"></param> /// <param name="allowSpaces">Whether spaces should be preserved.</param>
/// <returns></returns> /// <returns>A sanitized string that is safe to use as a path segment.</returns>
public static string ToPath(this string str, bool allowSpaces = false) public static string ToPath(this string str, bool allowSpaces = false)
{ {
char[] invalidChars = Path.GetInvalidPathChars(); return Sanitize(str, Path.GetInvalidPathChars(), allowSpaces);
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);
} }
/// <summary> /// <summary>
/// Truncates a <see cref="string"/> to the specified length. /// Returns a truncated version of the string with a maximum length.
/// </summary> /// </summary>
/// <param name="str"></param> /// <param name="str">The input string.</param>
/// <param name="length"></param> /// <param name="length">The maximum allowed length.</param>
/// <returns></returns> /// <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) public static string Truncate(this string str, int length)
{ {
if (str.Length > length && length > 0) return (length > 0 && str.Length > length) ? str[..length] : str;
{
return str[..length];
} }
/// <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; 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) if (ellipsis && length > 3)
{ {
return $"{str[..(length - 3)]}..."; return $"{str[..(length - 3)]}...";
} }
else
{
return str[..length]; return str[..length];
} }
}
return str;
}
/// <summary> /// <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> /// </summary>
/// <param name="str"></param> /// <param name="str">The input string.</param>
/// <returns></returns> /// <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) public static string CorrectNewLine(this string str)
{ {
if (Environment.OSVersion.Platform == PlatformID.Unix) return str
{ .Replace("\r\n", "\n")
str = str.Replace("\r\n", "\n"); .Replace("\r", "\n")
} .Replace("\n", Environment.NewLine);
else
{
str = str.Replace("\n", "\r\n"); //Ik that this can produce wrong results
}
return str;
} }
/// <summary> /// <summary>
/// Removes all white spaces from the specified <see cref="string"/> /// Removes all whitespace characters from the string.
/// </summary> /// </summary>
/// <param name="str"></param> /// <param name="str">The input string.</param>
/// <returns></returns> /// <returns>The string with all whitespace removed.</returns>
public static string RemoveWhitespaces(this string str) public static string RemoveWhitespaces(this string str)
{ {
StringBuilder result = new StringBuilder(); return new string(str.Where(c => !char.IsWhiteSpace(c)).ToArray());
foreach (char c in str)
{
if (!char.IsWhiteSpace(c))
{
_ = result.Append(c);
}
}
return result.ToString();
} }
/// <summary> /// <summary>
/// Reverses the specified <see cref="string"/> /// Returns a new string with the characters reversed.
/// </summary> /// </summary>
/// <param name="str"></param> /// <param name="str">The input string.</param>
/// <returns></returns> /// <returns>The reversed string.</returns>
public static string Reverse(this string str) public static string Reverse(this string str)
{ {
char[] array = str.ToCharArray(); char[] arr = str.ToCharArray();
Array.Reverse(array); Array.Reverse(arr);
return new string(array); 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);
} }
} }
+1 -1
View File
@@ -124,7 +124,7 @@ public static class Try
catch (Exception ex) catch (Exception ex)
{ {
lastException = ex; lastException = ex;
System.Threading.Thread.Sleep(delay.Value); Thread.Sleep(delay.Value);
} }
} }
throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException); throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
+27 -15
View File
@@ -1,4 +1,6 @@
namespace CuteUtils.Misc; using System.Diagnostics;
namespace CuteUtils.Misc;
/// <summary> /// <summary>
/// Provides utility methods for waiting on conditions or events with optional timeouts and intervals. /// Provides utility methods for waiting on conditions or events with optional timeouts and intervals.
@@ -16,16 +18,21 @@ public static class Wait
{ {
timeout ??= TimeSpan.MaxValue; timeout ??= TimeSpan.MaxValue;
interval ??= TimeSpan.FromMilliseconds(100); 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); 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.");
} }
}
/// <summary> /// <summary>
/// Synchronously waits until the specified condition is met or the timeout is reached. /// Synchronously waits until the specified condition is met or the timeout is reached.
@@ -38,16 +45,21 @@ public static class Wait
{ {
timeout ??= TimeSpan.MaxValue; timeout ??= TimeSpan.MaxValue;
interval ??= TimeSpan.FromMilliseconds(100); 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())
{
return;
} }
if (!condition())
{ Thread.Sleep(interval.Value);
}
throw new TimeoutException("The condition was not met within the specified timeout."); throw new TimeoutException("The condition was not met within the specified timeout.");
} }
}
/// <summary> /// <summary>
/// Asynchronously waits for an event to be raised or the timeout to be reached. /// Asynchronously waits for an event to be raised or the timeout to be reached.
@@ -88,7 +100,7 @@ public static class Wait
/// <typeparam name="T">The type of the event argument.</typeparam> /// <typeparam name="T">The type of the event argument.</typeparam>
/// <returns>A task that completes with the event argument when the event is raised.</returns> /// <returns>A task that completes with the event argument when the event is raised.</returns>
/// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception> /// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception>
public static async Task<T> WaitForEventAsync<T>(Action<Action<T>> subscribe, TimeSpan? timeout = null) public static async Task<T> ForEventAsync<T>(Action<Action<T>> subscribe, TimeSpan? timeout = null)
{ {
timeout ??= TimeSpan.MaxValue; timeout ??= TimeSpan.MaxValue;
@@ -121,7 +133,7 @@ public static class Wait
/// <param name="subscribe">An action that subscribes a handler to the event.</param> /// <param name="subscribe">An action that subscribes a handler to the event.</param>
/// <param name="timeout">The maximum time to wait. Defaults to infinite.</param> /// <param name="timeout">The maximum time to wait. Defaults to infinite.</param>
/// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception> /// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception>
public static void WaitForEvent(Action<Action> subscribe, TimeSpan? timeout = null) public static void ForEvent(Action<Action> subscribe, TimeSpan? timeout = null)
{ {
timeout ??= TimeSpan.MaxValue; timeout ??= TimeSpan.MaxValue;
TaskCompletionSource tcs = new TaskCompletionSource(); TaskCompletionSource tcs = new TaskCompletionSource();
@@ -152,7 +164,7 @@ public static class Wait
/// <typeparam name="T">The type of the event argument.</typeparam> /// <typeparam name="T">The type of the event argument.</typeparam>
/// <returns>The event argument when the event is raised.</returns> /// <returns>The event argument when the event is raised.</returns>
/// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception> /// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception>
public static T WaitForEvent<T>(Action<Action<T>> subscribe, TimeSpan? timeout = null) public static T ForEvent<T>(Action<Action<T>> subscribe, TimeSpan? timeout = null)
{ {
timeout ??= TimeSpan.MaxValue; timeout ??= TimeSpan.MaxValue;
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>(); TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
+94
View File
@@ -0,0 +1,94 @@
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
namespace CuteUtils.Misc;
/// <summary>
/// Provides simple XML serialization and deserialization helpers and schema generation helpers.
/// </summary>
public static class XmlHelper
{
/// <summary>
/// Deserializes an XML string into an instance of <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The target reference type to deserialize into. Must be a class.</typeparam>
/// <param name="xml">The XML string to deserialize. Must not be null.</param>
/// <param name="settings">Optional <see cref="XmlReaderSettings"/> to control reader behavior. If null, default settings are used.</param>
/// <returns>An instance of <typeparamref name="T"/> if deserialization succeeds; otherwise, <c>null</c>.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="xml"/> is null.</exception>
/// <exception cref="InvalidOperationException">May be thrown by <see cref="XmlSerializer"/> when deserialization fails.</exception>
public static T? Deserialize<T>(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;
}
/// <summary>
/// Serializes an object to its XML representation.
/// </summary>
/// <typeparam name="T">The type of the object to serialize.</typeparam>
/// <param name="obj">The object instance to serialize. Must not be null.</param>
/// <param name="settings">Optional <see cref="XmlWriterSettings"/> to control writer behavior. If null, default settings are used.</param>
/// <param name="xmlSerializerNamespaces">Optional <see cref="XmlSerializerNamespaces"/> to control namespace declarations. If null, an empty namespace is used to omit declarations.</param>
/// <returns>A string containing the XML representation of <paramref name="obj"/>.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="obj"/> is null.</exception>
/// <exception cref="InvalidOperationException">May be thrown by <see cref="XmlSerializer"/> when serialization fails.</exception>
public static string Serialize<T>(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();
}
/// <summary>
/// Generates XML schema definitions for the specified type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type for which to generate XML schema.</typeparam>
/// <param name="settings">Optional <see cref="XmlWriterSettings"/> to control output formatting. If null, default settings are used.</param>
/// <returns>A string containing one or more XML schema documents representing <typeparamref name="T"/>.</returns>
/// <exception cref="InvalidOperationException">May be thrown when schema export fails.</exception>
public static string GetXmlSchema<T>(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();
}
}