mirror of
https://github.com/Stone-Red-Code/CuteUtils.git
synced 2026-09-04 17:06:10 +02:00
Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9bbb6c739 | ||
|
|
131eb22264 | ||
|
|
7dd6a3dca6 | ||
|
|
82301b3685 | ||
|
|
d47e41d1d5 | ||
|
|
b136dda50b | ||
|
|
d588b6049b | ||
|
|
7184b6b15c | ||
|
|
864604b36c | ||
|
|
981ec07ce1 | ||
|
|
066175ed24 | ||
|
|
50266440fa | ||
|
|
c49e29ac7d | ||
|
|
f366290d5d | ||
|
|
d5581cae96 | ||
|
|
e7e953d6b1 | ||
|
|
5f4d77d3b3 | ||
|
|
5291a75b67 | ||
|
|
a14d97f578 | ||
|
|
1e5162eb14 | ||
|
|
c5773eab97 | ||
|
|
abff6a79fa | ||
|
|
536ff7c838 | ||
|
|
df93a5cce0 | ||
|
|
07ec1bd851 | ||
|
|
45571d7123 | ||
|
|
e9d1f26df2 | ||
|
|
61ea228325 |
@@ -1,9 +1,9 @@
|
||||
# CuteUtils
|
||||
> A "cute" utility library for C#
|
||||
|
||||
# Setup
|
||||
## Setup
|
||||
**Package Manager:** `Install-Package CuteUtils`\
|
||||
**.NET CLI:** `dotnet add package CuteUtils`
|
||||
|
||||
# Wiki
|
||||
## Wiki
|
||||
**Reference:** https://github.com/Stone-Red-Code/CuteUtils/wiki/Reference
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -10,10 +10,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="4.0.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="4.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("Blocker Code Smell", "S2699:Tests should include assertions", Justification = "<Pending>")]
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
namespace CuteUtils.Tests.Misc;
|
||||
|
||||
[TestClass]
|
||||
public class BoolExtentionsTests
|
||||
public class BoolExtensionsTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void OneWayTrue_ShouldSetValueToTrue_WhenInputIsTrue()
|
||||
+1
-1
@@ -5,7 +5,7 @@ using System.Text;
|
||||
namespace CuteUtils.Tests.Misc;
|
||||
|
||||
[TestClass]
|
||||
public class CollectionExtentionsTests
|
||||
public class CollectionExtensionsTests
|
||||
{
|
||||
private StringBuilder consoleOutput = null!;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
using CuteUtils.Misc;
|
||||
|
||||
namespace CuteUtils.Tests.Misc;
|
||||
|
||||
[TestClass]
|
||||
public class TryTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task RetryAsyncT_SucceedsFirstTry()
|
||||
{
|
||||
int callCount = 0;
|
||||
int result = await Try.RetryAsync(async () =>
|
||||
{
|
||||
callCount++;
|
||||
await Task.Delay(10);
|
||||
return 42;
|
||||
});
|
||||
Assert.AreEqual(1, callCount);
|
||||
Assert.AreEqual(42, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task RetryAsyncT_RetriesAndSucceeds()
|
||||
{
|
||||
int callCount = 0;
|
||||
int result = await Try.RetryAsync(async () =>
|
||||
{
|
||||
callCount++;
|
||||
if (callCount < 3)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
await Task.Delay(10);
|
||||
return 99;
|
||||
}, maxRetries: 5, delay: TimeSpan.FromMilliseconds(1));
|
||||
Assert.AreEqual(3, callCount);
|
||||
Assert.AreEqual(99, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task RetryAsyncT_ThrowsAfterMaxRetries()
|
||||
{
|
||||
int callCount = 0;
|
||||
_ = await Assert.ThrowsExactlyAsync<InvalidOperationException>(async () =>
|
||||
{
|
||||
_ = await Try.RetryAsync<int>(() =>
|
||||
{
|
||||
callCount++;
|
||||
throw new InvalidOperationException("fail");
|
||||
}, maxRetries: 2, delay: TimeSpan.FromMilliseconds(1));
|
||||
});
|
||||
Assert.AreEqual(2, callCount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task RetryAsync_SucceedsFirstTry()
|
||||
{
|
||||
int callCount = 0;
|
||||
await Try.RetryAsync(async () =>
|
||||
{
|
||||
callCount++;
|
||||
await Task.Delay(10);
|
||||
});
|
||||
Assert.AreEqual(1, callCount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task RetryAsync_RetriesAndSucceeds()
|
||||
{
|
||||
int callCount = 0;
|
||||
await Try.RetryAsync(async () =>
|
||||
{
|
||||
callCount++;
|
||||
if (callCount < 2)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
await Task.Delay(10);
|
||||
}, maxRetries: 3, delay: TimeSpan.FromMilliseconds(1));
|
||||
Assert.AreEqual(2, callCount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task RetryAsync_ThrowsAfterMaxRetries()
|
||||
{
|
||||
int callCount = 0;
|
||||
_ = await Assert.ThrowsExactlyAsync<InvalidOperationException>(async () =>
|
||||
{
|
||||
await Try.RetryAsync(() =>
|
||||
{
|
||||
callCount++;
|
||||
throw new Exception();
|
||||
}, maxRetries: 2, delay: TimeSpan.FromMilliseconds(1));
|
||||
});
|
||||
Assert.AreEqual(2, callCount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RetryT_SucceedsFirstTry()
|
||||
{
|
||||
int callCount = 0;
|
||||
int result = Try.Retry(() =>
|
||||
{
|
||||
callCount++;
|
||||
return 7;
|
||||
});
|
||||
Assert.AreEqual(1, callCount);
|
||||
Assert.AreEqual(7, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RetryT_RetriesAndSucceeds()
|
||||
{
|
||||
int callCount = 0;
|
||||
int result = Try.Retry(() =>
|
||||
{
|
||||
callCount++;
|
||||
if (callCount < 4)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
return 123;
|
||||
}, maxRetries: 5, delay: TimeSpan.FromMilliseconds(1));
|
||||
Assert.AreEqual(4, callCount);
|
||||
Assert.AreEqual(123, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RetryT_ThrowsAfterMaxRetries()
|
||||
{
|
||||
int callCount = 0;
|
||||
_ = Assert.ThrowsExactly<InvalidOperationException>(() =>
|
||||
{
|
||||
_ = Try.Retry<int>(() =>
|
||||
{
|
||||
callCount++;
|
||||
throw new Exception();
|
||||
}, maxRetries: 3, delay: TimeSpan.FromMilliseconds(1));
|
||||
});
|
||||
Assert.AreEqual(3, callCount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Retry_SucceedsFirstTry()
|
||||
{
|
||||
int callCount = 0;
|
||||
Try.Retry(() =>
|
||||
{
|
||||
callCount++;
|
||||
});
|
||||
Assert.AreEqual(1, callCount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Retry_RetriesAndSucceeds()
|
||||
{
|
||||
int callCount = 0;
|
||||
Try.Retry(() =>
|
||||
{
|
||||
callCount++;
|
||||
if (callCount < 2)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
}, maxRetries: 3, delay: TimeSpan.FromMilliseconds(1));
|
||||
Assert.AreEqual(2, callCount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Retry_ThrowsAfterMaxRetries()
|
||||
{
|
||||
int callCount = 0;
|
||||
_ = Assert.ThrowsExactly<InvalidOperationException>(() =>
|
||||
{
|
||||
Try.Retry(() =>
|
||||
{
|
||||
callCount++;
|
||||
throw new Exception();
|
||||
}, maxRetries: 2, delay: TimeSpan.FromMilliseconds(1));
|
||||
});
|
||||
Assert.AreEqual(2, callCount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task RetryAsyncT_ThrowsArgumentNullException()
|
||||
{
|
||||
_ = await Assert.ThrowsExactlyAsync<ArgumentNullException>(async () =>
|
||||
{
|
||||
_ = await Try.RetryAsync<int>(null!);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task RetryAsync_ThrowsArgumentNullException()
|
||||
{
|
||||
_ = await Assert.ThrowsExactlyAsync<ArgumentNullException>(async () =>
|
||||
{
|
||||
await Try.RetryAsync(null!);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RetryT_ThrowsArgumentNullException()
|
||||
{
|
||||
_ = Assert.ThrowsExactly<ArgumentNullException>(() =>
|
||||
{
|
||||
_ = Try.Retry<int>(null!);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Retry_ThrowsArgumentNullException()
|
||||
{
|
||||
_ = Assert.ThrowsExactly<ArgumentNullException>(() =>
|
||||
{
|
||||
Try.Retry(null!);
|
||||
});
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class TryCatchTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Catch_Action_NoException()
|
||||
{
|
||||
bool executed = false;
|
||||
|
||||
_ = Try.Catch(() => executed = true);
|
||||
|
||||
Assert.IsTrue(executed);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Catch_Action_ExceptionHandled()
|
||||
{
|
||||
Try.Catch(() => throw new InvalidOperationException());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Catch_Action_OutParam_ExceptionHandled()
|
||||
{
|
||||
Try.Catch(() => throw new InvalidOperationException(), out Exception? exception);
|
||||
|
||||
Assert.IsNotNull(exception);
|
||||
Assert.IsInstanceOfType(exception, typeof(InvalidOperationException));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Catch_TFunc_NoException()
|
||||
{
|
||||
int result = Try.Catch(() => 42)!;
|
||||
|
||||
Assert.AreEqual(42, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Catch_TFunc_ExceptionHandled_ReturnsDefault()
|
||||
{
|
||||
int? result = Try.Catch<int>(() => throw new InvalidOperationException());
|
||||
|
||||
Assert.AreEqual(default(int), result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Catch_TFunc_OutParam_ExceptionHandled()
|
||||
{
|
||||
int? result = Try.Catch<int?>(() => throw new InvalidOperationException(), out Exception? exception);
|
||||
|
||||
Assert.IsNull(result);
|
||||
Assert.IsNotNull(exception);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CatchAsync_Delegate_NoException()
|
||||
{
|
||||
bool executed = false;
|
||||
|
||||
await Try.CatchAsync(async () =>
|
||||
{
|
||||
executed = true;
|
||||
await Task.Delay(10);
|
||||
});
|
||||
|
||||
Assert.IsTrue(executed);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CatchAsync_Delegate_ExceptionHandled()
|
||||
{
|
||||
await Try.CatchAsync(async () =>
|
||||
{
|
||||
await Task.Delay(10);
|
||||
throw new InvalidOperationException();
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CatchAsync_TFunc_NoException()
|
||||
{
|
||||
int result = await Try.CatchAsync(async () =>
|
||||
{
|
||||
await Task.Delay(10);
|
||||
return 99;
|
||||
});
|
||||
|
||||
Assert.AreEqual(99, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CatchAsync_TFunc_ExceptionHandled_ReturnsDefault()
|
||||
{
|
||||
int result = await Try.CatchAsync<int>(async () =>
|
||||
{
|
||||
await Task.Delay(10);
|
||||
throw new InvalidOperationException();
|
||||
});
|
||||
|
||||
Assert.AreEqual(default, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CatchAsync_Task_NoException()
|
||||
{
|
||||
await Try.CatchAsync(Task.Delay(10));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CatchAsync_Task_ExceptionHandled()
|
||||
{
|
||||
await Try.CatchAsync(Task.Run(() => throw new InvalidOperationException()));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CatchAsync_TaskT_NoException()
|
||||
{
|
||||
string? result = await Try.CatchAsync(Task.FromResult("Hello"));
|
||||
|
||||
Assert.AreEqual("Hello", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CatchAsync_TaskT_ExceptionHandled_ReturnsDefault()
|
||||
{
|
||||
string? result = await Try.CatchAsync<string>(Task.Run<string>(() => ""[0].ToString()));
|
||||
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using CuteUtils.Misc;
|
||||
|
||||
namespace CuteUtils.Tests.Misc;
|
||||
|
||||
[TestClass]
|
||||
public class WaitTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task UntilAsync_ConditionMet_CompletesSuccessfully()
|
||||
{
|
||||
bool flag = false;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(200);
|
||||
flag = true;
|
||||
});
|
||||
await Wait.UntilAsync(() => flag, TimeSpan.FromMilliseconds(50), TimeSpan.FromSeconds(2));
|
||||
Assert.IsTrue(flag);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task UntilAsync_ConditionNotMet_ThrowsTimeoutException()
|
||||
{
|
||||
_ = await Assert.ThrowsExactlyAsync<TimeoutException>(async () =>
|
||||
{
|
||||
await Wait.UntilAsync(() => false, TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(200));
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Until_ConditionMet_CompletesSuccessfully()
|
||||
{
|
||||
bool flag = false;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(200);
|
||||
flag = true;
|
||||
});
|
||||
Wait.Until(() => flag, TimeSpan.FromMilliseconds(50));
|
||||
Assert.IsTrue(flag);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Until_ConditionNotMet_ThrowsTimeoutException()
|
||||
{
|
||||
_ = Assert.ThrowsExactly<TimeoutException>(() =>
|
||||
{
|
||||
Wait.Until(() => false, TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(200));
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task WaitForEventAsync_EventRaised_CompletesSuccessfully()
|
||||
{
|
||||
void subscribe(Action handler)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(200);
|
||||
handler();
|
||||
});
|
||||
}
|
||||
await Wait.WaitForEventAsync(subscribe, TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task WaitForEventAsync_EventNotRaised_ThrowsTimeoutException()
|
||||
{
|
||||
void subscribe(Action handler)
|
||||
{
|
||||
// Intentionally left empty for negative test case (event never raised)
|
||||
}
|
||||
_ = await Assert.ThrowsExactlyAsync<TimeoutException>(async () =>
|
||||
{
|
||||
await Wait.WaitForEventAsync(subscribe, TimeSpan.FromMilliseconds(200));
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task WaitForEventAsyncT_EventRaised_CompletesWithValue()
|
||||
{
|
||||
void subscribe(Action<int> handler)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(200);
|
||||
handler(42);
|
||||
});
|
||||
}
|
||||
int result = await Wait.ForEventAsync((Action<Action<int>>)subscribe, TimeSpan.FromSeconds(2));
|
||||
Assert.AreEqual(42, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task WaitForEventAsyncT_EventNotRaised_ThrowsTimeoutException()
|
||||
{
|
||||
void subscribe(Action<int> handler)
|
||||
{
|
||||
// Intentionally left empty for negative test case (event never raised)
|
||||
}
|
||||
_ = await Assert.ThrowsExactlyAsync<TimeoutException>(async () =>
|
||||
{
|
||||
_ = await Wait.ForEventAsync((Action<Action<int>>)subscribe, TimeSpan.FromMilliseconds(200));
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WaitForEvent_EventRaised_CompletesSuccessfully()
|
||||
{
|
||||
void subscribe(Action handler)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(200);
|
||||
handler();
|
||||
});
|
||||
}
|
||||
Wait.ForEvent(subscribe, TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WaitForEvent_EventNotRaised_ThrowsTimeoutException()
|
||||
{
|
||||
void subscribe(Action handler)
|
||||
{
|
||||
// Intentionally left empty for negative test case (event never raised)
|
||||
}
|
||||
_ = Assert.ThrowsExactly<TimeoutException>(() =>
|
||||
{
|
||||
Wait.ForEvent(subscribe, TimeSpan.FromMilliseconds(200));
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WaitForEventT_EventRaised_CompletesWithValue()
|
||||
{
|
||||
void subscribe(Action<string> handler)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(200);
|
||||
handler("hello");
|
||||
});
|
||||
}
|
||||
string result = Wait.ForEvent((Action<Action<string>>)subscribe, TimeSpan.FromSeconds(2));
|
||||
Assert.AreEqual("hello", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WaitForEventT_EventNotRaised_ThrowsTimeoutException()
|
||||
{
|
||||
void subscribe(Action<string> handler)
|
||||
{
|
||||
// Intentionally left empty for negative test case (event never raised)
|
||||
}
|
||||
_ = Assert.ThrowsExactly<TimeoutException>(() =>
|
||||
{
|
||||
_ = 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.");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
@@ -29,11 +29,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Reactive.Linq" Version="6.0.0" />
|
||||
<PackageReference Include="Vsxmd" Version="1.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Reactive.Linq" Version="6.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
@@ -44,7 +40,7 @@
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<UserTempFolder>$([System.IO.Path]::GetTempPath())</UserTempFolder>
|
||||
<Version>1.0.0.0</Version>
|
||||
<Version>2.0.0.0</Version>
|
||||
<Description>A "cute" utility library for C#</Description>
|
||||
<RepositoryUrl>https://github.com/Stone-Red-Code/CuteUtils</RepositoryUrl>
|
||||
<PackageTags>Utility, Helper</PackageTags>
|
||||
|
||||
+9
-9
@@ -1,4 +1,4 @@
|
||||
namespace CuteUtils.FluentMath.TypeExtentions;
|
||||
namespace CuteUtils.FluentMath.TypeExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// DecimalFluent class
|
||||
@@ -56,7 +56,7 @@ public static class DecimalFluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the two nums
|
||||
/// Adds the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -67,7 +67,7 @@ public static class DecimalFluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the two nums
|
||||
/// Subtracts the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -78,7 +78,7 @@ public static class DecimalFluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiples the two nums
|
||||
/// Multiples the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -89,7 +89,7 @@ public static class DecimalFluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the two nums
|
||||
/// Divides the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -136,15 +136,15 @@ public static class DecimalFluent
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Math.Round(decimal,int)"/>
|
||||
public static decimal Round(this decimal num, int digits)
|
||||
public static decimal Round(this decimal num, int decimals)
|
||||
{
|
||||
return Math.Round(num, digits);
|
||||
return Math.Round(num, decimals);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Math.Round(decimal,int,MidpointRounding)"/>
|
||||
public static decimal Round(this decimal num, int digits, MidpointRounding mode)
|
||||
public static decimal Round(this decimal num, int decimals, MidpointRounding mode)
|
||||
{
|
||||
return Math.Round(num, digits, mode);
|
||||
return Math.Round(num, decimals, mode);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Math.Sign(decimal)"/>
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
namespace CuteUtils.FluentMath.TypeExtentions;
|
||||
namespace CuteUtils.FluentMath.TypeExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// DoubleFluent class
|
||||
@@ -190,9 +190,9 @@ public static class DoubleFluent
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Math.IEEERemainder(double,double)"/>
|
||||
public static double IEEERemainder(this double num, double valuee)
|
||||
public static double IEEERemainder(this double num, double value)
|
||||
{
|
||||
return Math.IEEERemainder(num, valuee);
|
||||
return Math.IEEERemainder(num, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Math.Log(double)"/>
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
namespace CuteUtils.FluentMath.TypeExtentions;
|
||||
namespace CuteUtils.FluentMath.TypeExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// IntegerFluent class
|
||||
@@ -56,7 +56,7 @@ public static class Int16Fluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the two nums
|
||||
/// Adds the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -67,7 +67,7 @@ public static class Int16Fluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the two nums
|
||||
/// Subtracts the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -78,7 +78,7 @@ public static class Int16Fluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiples the two nums
|
||||
/// Multiples the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -89,7 +89,7 @@ public static class Int16Fluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the two nums
|
||||
/// Divides the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
namespace CuteUtils.FluentMath.TypeExtentions;
|
||||
namespace CuteUtils.FluentMath.TypeExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// IntegerFluent class
|
||||
@@ -56,7 +56,7 @@ public static class Int32Fluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the two nums
|
||||
/// Adds the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -67,7 +67,7 @@ public static class Int32Fluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the two nums
|
||||
/// Subtracts the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -78,7 +78,7 @@ public static class Int32Fluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiples the two nums
|
||||
/// Multiples the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -89,7 +89,7 @@ public static class Int32Fluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the two nums
|
||||
/// Divides the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
namespace CuteUtils.FluentMath.TypeExtentions;
|
||||
namespace CuteUtils.FluentMath.TypeExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// IntegerFluent class
|
||||
@@ -56,7 +56,7 @@ public static class Int64Fluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the two nums
|
||||
/// Adds the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -67,7 +67,7 @@ public static class Int64Fluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the two nums
|
||||
/// Subtracts the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -78,7 +78,7 @@ public static class Int64Fluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiples the two nums
|
||||
/// Multiples the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -89,7 +89,7 @@ public static class Int64Fluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the two nums
|
||||
/// Divides the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
namespace CuteUtils.FluentMath.TypeExtentions;
|
||||
namespace CuteUtils.FluentMath.TypeExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// FloatFluent class
|
||||
@@ -56,7 +56,7 @@ public static class SingleFluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the two nums
|
||||
/// Adds the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -67,7 +67,7 @@ public static class SingleFluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the two nums
|
||||
/// Subtracts the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -78,7 +78,7 @@ public static class SingleFluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiples the two nums
|
||||
/// Multiples the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -89,7 +89,7 @@ public static class SingleFluent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the two nums
|
||||
/// Divides the two numbers
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="value"></param>
|
||||
@@ -19,7 +19,7 @@ public class LogFormatBuilder
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="LogFormatBuilder"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="value">The inital format.</param>
|
||||
/// <param name="value">The initial format.</param>
|
||||
public LogFormatBuilder(string value)
|
||||
{
|
||||
_ = stringBuilder.Append(value);
|
||||
@@ -55,7 +55,7 @@ public class LogFormatBuilder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the log datie time to the log format.
|
||||
/// Appends the log date time to the log format.
|
||||
/// </summary>
|
||||
/// <param name="format">The format to apply.</param>
|
||||
/// <param name="padding">The padding to apply.</param>
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace CuteUtils.Misc;
|
||||
/// <see cref="Console"/> Extensions
|
||||
/// </summary>
|
||||
public static class ConsoleExt
|
||||
{
|
||||
extension(Console)
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes the specified value to the console with the specified color.
|
||||
@@ -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>
|
||||
/// Writes the specified value to the console with the specified color and appends a new line.
|
||||
/// </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>
|
||||
/// Reads the next line of characters from the standard input stream and tries to convert it to the specified type.
|
||||
/// </summary>
|
||||
@@ -145,3 +159,4 @@ public static class ConsoleExt
|
||||
_ = Console.ReadKey(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace CuteUtils.Misc;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// 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">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)
|
||||
{
|
||||
string path = Sanitize(str, Path.GetInvalidFileNameChars(), allowSpaces);
|
||||
|
||||
if (path.Length > 255)
|
||||
{
|
||||
path = path[..255];
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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">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)
|
||||
{
|
||||
return Sanitize(str, Path.GetInvalidPathChars(), allowSpaces);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a truncated version of the string with a maximum length.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
if (ellipsis && length > 3)
|
||||
{
|
||||
return $"{str[..(length - 3)]}...";
|
||||
}
|
||||
|
||||
return str[..length];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes newline characters in the string to the current environment's newline format.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
return str
|
||||
.Replace("\r\n", "\n")
|
||||
.Replace("\r", "\n")
|
||||
.Replace("\n", Environment.NewLine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all whitespace characters from the string.
|
||||
/// </summary>
|
||||
/// <param name="str">The input string.</param>
|
||||
/// <returns>The string with all whitespace removed.</returns>
|
||||
public static string RemoveWhitespaces(this string str)
|
||||
{
|
||||
return new string(str.Where(c => !char.IsWhiteSpace(c)).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new string with the characters reversed.
|
||||
/// </summary>
|
||||
/// <param name="str">The input string.</param>
|
||||
/// <returns>The reversed string.</returns>
|
||||
public static string Reverse(this string str)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CuteUtils.Misc;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods for safely retrying actions and functions with optional delay and retry count.
|
||||
/// </summary>
|
||||
public static class Try
|
||||
{
|
||||
/// <summary>
|
||||
/// Retries an asynchronous function returning a value, up to a specified number of times, with an optional delay between attempts.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result returned by the function.</typeparam>
|
||||
/// <param name="action">The asynchronous function to execute.</param>
|
||||
/// <param name="maxRetries">The maximum number of retry attempts. Default is 3.</param>
|
||||
/// <param name="delay">The delay between retry attempts. Default is 1 second.</param>
|
||||
/// <returns>The result of the function if successful.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is null.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown if all retry attempts fail.</exception>
|
||||
public static async Task<T> RetryAsync<T>(Func<Task<T>> action, int maxRetries = 3, TimeSpan? delay = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
|
||||
delay ??= TimeSpan.FromSeconds(1);
|
||||
Exception? lastException = null;
|
||||
for (int i = 0; i < maxRetries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
await Task.Delay(delay.Value);
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retries an asynchronous action up to a specified number of times, with an optional delay between attempts.
|
||||
/// </summary>
|
||||
/// <param name="action">The asynchronous action to execute.</param>
|
||||
/// <param name="maxRetries">The maximum number of retry attempts. Default is 3.</param>
|
||||
/// <param name="delay">The delay between retry attempts. Default is 1 second.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is null.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown if all retry attempts fail.</exception>
|
||||
public static async Task RetryAsync(Func<Task> action, int maxRetries = 3, TimeSpan? delay = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
|
||||
delay ??= TimeSpan.FromSeconds(1);
|
||||
Exception? lastException = null;
|
||||
for (int i = 0; i < maxRetries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await action();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
await Task.Delay(delay.Value);
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retries a synchronous function returning a value, up to a specified number of times, with an optional delay between attempts.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result returned by the function.</typeparam>
|
||||
/// <param name="action">The function to execute.</param>
|
||||
/// <param name="maxRetries">The maximum number of retry attempts. Default is 3.</param>
|
||||
/// <param name="delay">The delay between retry attempts. Default is 1 second.</param>
|
||||
/// <returns>The result of the function if successful.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is null.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown if all retry attempts fail.</exception>
|
||||
public static T Retry<T>(Func<T> action, int maxRetries = 3, TimeSpan? delay = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
|
||||
delay ??= TimeSpan.FromSeconds(1);
|
||||
Exception? lastException = null;
|
||||
for (int i = 0; i < maxRetries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
return action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
System.Threading.Thread.Sleep(delay.Value);
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retries a synchronous action up to a specified number of times, with an optional delay between attempts.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
/// <param name="maxRetries">The maximum number of retry attempts. Default is 3.</param>
|
||||
/// <param name="delay">The delay between retry attempts. Default is 1 second.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is null.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown if all retry attempts fail.</exception>
|
||||
public static void Retry(Action action, int maxRetries = 3, TimeSpan? delay = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
|
||||
delay ??= TimeSpan.FromSeconds(1);
|
||||
Exception? lastException = null;
|
||||
for (int i = 0; i < maxRetries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
Thread.Sleep(delay.Value);
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a synchronous action and catches any exception, writing it to the debug output.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
public static void Catch(Action action)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a synchronous action and catches any exception, writing it to the debug output and returning it via an out parameter.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
/// <param name="exception">When this method returns, contains the exception that was thrown, or null if no exception was thrown.</param>
|
||||
public static void Catch(Action action, out Exception? exception)
|
||||
{
|
||||
exception = null;
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
exception = ex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a synchronous function and catches any exception, writing it to the debug output. Returns the default value if an exception occurs.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the function.</typeparam>
|
||||
/// <param name="action">The function to execute.</param>
|
||||
/// <returns>The result of the function, or the default value of <typeparamref name="T"/> if an exception occurs.</returns>
|
||||
public static T? Catch<T>(Func<T> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
return action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a synchronous function and catches any exception, writing it to the debug output and returning the exception via an out parameter.
|
||||
/// Returns the default value if an exception occurs.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the function.</typeparam>
|
||||
/// <param name="action">The function to execute.</param>
|
||||
/// <param name="exception">When this method returns, contains the exception that was thrown, or null if no exception was thrown.</param>
|
||||
/// <returns>The result of the function, or the default value of <typeparamref name="T"/> if an exception occurs.</returns>
|
||||
public static T? Catch<T>(Func<T> action, out Exception? exception)
|
||||
{
|
||||
exception = null;
|
||||
try
|
||||
{
|
||||
return action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
exception = ex;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes an asynchronous action and catches any exception, writing it to the debug output.
|
||||
/// </summary>
|
||||
/// <param name="action">The asynchronous action to execute.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task CatchAsync(Func<Task> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
await action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes an asynchronous function and catches any exception, writing it to the debug output.
|
||||
/// Returns the default value if an exception occurs.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the function.</typeparam>
|
||||
/// <param name="action">The asynchronous function to execute.</param>
|
||||
/// <returns>The result of the function, or the default value of <typeparamref name="T"/> if an exception occurs.</returns>
|
||||
public static async Task<T?> CatchAsync<T>(Func<Task<T>> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for an asynchronous task to complete and catches any exception, writing it to the debug output.
|
||||
/// </summary>
|
||||
/// <param name="task">The task to await.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task CatchAsync(Task task)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for an asynchronous task that returns a result and catches any exception, writing it to the debug output.
|
||||
/// Returns the default value if an exception occurs.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the task.</typeparam>
|
||||
/// <param name="task">The task to await.</param>
|
||||
/// <returns>The result of the task, or the default value of <typeparamref name="T"/> if an exception occurs.</returns>
|
||||
public static async Task<T?> CatchAsync<T>(Task<T> task)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await task;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CuteUtils.Misc;
|
||||
|
||||
/// <summary>
|
||||
/// Provides utility methods for waiting on conditions or events with optional timeouts and intervals.
|
||||
/// </summary>
|
||||
public static class Wait
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously waits until the specified condition is met or the timeout is reached.
|
||||
/// </summary>
|
||||
/// <param name="condition">A function that returns true when the wait should end.</param>
|
||||
/// <param name="interval">The interval to check the condition. Defaults to 100ms.</param>
|
||||
/// <param name="timeout">The maximum time to wait. Defaults to infinite.</param>
|
||||
/// <exception cref="TimeoutException">Thrown if the condition is not met within the timeout.</exception>
|
||||
public static async Task UntilAsync(Func<bool> condition, TimeSpan? interval = null, TimeSpan? timeout = null)
|
||||
{
|
||||
timeout ??= TimeSpan.MaxValue;
|
||||
interval ??= TimeSpan.FromMilliseconds(100);
|
||||
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
|
||||
while (sw.Elapsed < timeout.Value)
|
||||
{
|
||||
if (condition())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(interval.Value);
|
||||
}
|
||||
|
||||
throw new TimeoutException("The condition was not met within the specified timeout.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously waits until the specified condition is met or the timeout is reached.
|
||||
/// </summary>
|
||||
/// <param name="condition">A function that returns true when the wait should end.</param>
|
||||
/// <param name="interval">The interval to check the condition. Defaults to 100ms.</param>
|
||||
/// <param name="timeout">The maximum time to wait. Defaults to infinite.</param>
|
||||
/// <exception cref="TimeoutException">Thrown if the condition is not met within the timeout.</exception>
|
||||
public static void Until(Func<bool> condition, TimeSpan? interval = null, TimeSpan? timeout = null)
|
||||
{
|
||||
timeout ??= TimeSpan.MaxValue;
|
||||
interval ??= TimeSpan.FromMilliseconds(100);
|
||||
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
|
||||
while (sw.Elapsed < timeout.Value)
|
||||
{
|
||||
if (condition())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Thread.Sleep(interval.Value);
|
||||
}
|
||||
|
||||
throw new TimeoutException("The condition was not met within the specified timeout.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously waits for an event to be raised or the timeout to be reached.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception>
|
||||
public static async Task WaitForEventAsync(Action<Action> subscribe, TimeSpan? timeout = null)
|
||||
{
|
||||
timeout ??= TimeSpan.MaxValue;
|
||||
TaskCompletionSource tcs = new TaskCompletionSource();
|
||||
void Handler()
|
||||
{
|
||||
_ = tcs.TrySetResult();
|
||||
}
|
||||
|
||||
subscribe(Handler);
|
||||
|
||||
using CancellationTokenSource cts = new CancellationTokenSource(timeout.Value);
|
||||
using (cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)))
|
||||
{
|
||||
try
|
||||
{
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw new TimeoutException("The event was not raised within the specified timeout.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously waits for an event to be raised or the timeout to be reached.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <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>
|
||||
/// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception>
|
||||
public static async Task<T> ForEventAsync<T>(Action<Action<T>> subscribe, TimeSpan? timeout = null)
|
||||
{
|
||||
timeout ??= TimeSpan.MaxValue;
|
||||
|
||||
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
|
||||
|
||||
void Handler(T arg)
|
||||
{
|
||||
_ = tcs.TrySetResult(arg);
|
||||
}
|
||||
|
||||
subscribe(Handler);
|
||||
|
||||
using CancellationTokenSource cts = new CancellationTokenSource(timeout.Value);
|
||||
using (cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)))
|
||||
{
|
||||
try
|
||||
{
|
||||
return await tcs.Task;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw new TimeoutException("The event was not raised within the specified timeout.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously waits for an event to be raised or the timeout to be reached.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception>
|
||||
public static void ForEvent(Action<Action> subscribe, TimeSpan? timeout = null)
|
||||
{
|
||||
timeout ??= TimeSpan.MaxValue;
|
||||
TaskCompletionSource tcs = new TaskCompletionSource();
|
||||
void Handler()
|
||||
{
|
||||
_ = tcs.TrySetResult();
|
||||
}
|
||||
subscribe(Handler);
|
||||
using CancellationTokenSource cts = new CancellationTokenSource(timeout.Value);
|
||||
using (cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)))
|
||||
{
|
||||
try
|
||||
{
|
||||
tcs.Task.Wait();
|
||||
}
|
||||
catch (AggregateException ex) when (ex.InnerException is TaskCanceledException)
|
||||
{
|
||||
throw new TimeoutException("The event was not raised within the specified timeout.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously waits for an event to be raised or the timeout to be reached.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <typeparam name="T">The type of the event argument.</typeparam>
|
||||
/// <returns>The event argument when the event is raised.</returns>
|
||||
/// <exception cref="TimeoutException">Thrown if the event is not raised within the timeout.</exception>
|
||||
public static T ForEvent<T>(Action<Action<T>> subscribe, TimeSpan? timeout = null)
|
||||
{
|
||||
timeout ??= TimeSpan.MaxValue;
|
||||
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
|
||||
void Handler(T arg)
|
||||
{
|
||||
_ = tcs.TrySetResult(arg);
|
||||
}
|
||||
subscribe(Handler);
|
||||
using CancellationTokenSource cts = new CancellationTokenSource(timeout.Value);
|
||||
using (cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)))
|
||||
{
|
||||
try
|
||||
{
|
||||
return tcs.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
catch (AggregateException ex) when (ex.InnerException is TaskCanceledException)
|
||||
{
|
||||
throw new TimeoutException("The event was not raised within the specified timeout.", ex);
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
throw new TimeoutException("The event was not raised within the specified timeout.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@ namespace CuteUtils.Reflection;
|
||||
/// <summary>
|
||||
/// Provides extension methods for reflection operations.
|
||||
/// </summary>
|
||||
public static class ReflectionExtentions
|
||||
public static class ReflectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the specified type and copies the properties from the source object to the new instance.
|
||||
@@ -1,169 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace CuteUtils;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="string"/> Extensions
|
||||
/// </summary>
|
||||
public static class StringExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Removes all invalid chars from the specified <see cref="string"/>
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="allowSpaces"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToFileName(this string str, bool allowSpaces = false)
|
||||
{
|
||||
char[] invalidChars = Path.GetInvalidFileNameChars();
|
||||
|
||||
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>
|
||||
/// Removes all invalid chars from the specified <see cref="string"/>
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="allowSpaces"></param>
|
||||
/// <returns></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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates a <see cref="string"/> to the specified length.
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="length"></param>
|
||||
/// <returns></returns>
|
||||
public static string Truncate(this string str, int length)
|
||||
{
|
||||
if (str.Length > length && length > 0)
|
||||
{
|
||||
return str[..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.
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all white spaces from the specified <see cref="string"/>
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverses the specified <see cref="string"/>
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static string Reverse(this string str)
|
||||
{
|
||||
char[] array = str.ToCharArray();
|
||||
Array.Reverse(array);
|
||||
return new string(array);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user