1
0
mirror of https://github.com/oliverbooth/X10D synced 2024-11-23 01:58:49 +00:00

Compare commits

..

No commits in common. "db96c9e6fbe96feb4ec82f71e3ec700a8430cf39" and "19c467d88b2b5ed551f43dc19b6c3a463c7091e5" have entirely different histories.

24 changed files with 309 additions and 344 deletions

View File

@ -24,6 +24,7 @@ jobs:
with: with:
dotnet-version: | dotnet-version: |
6.0.x 6.0.x
7.0.x
8.0.x 8.0.x
- name: Add NuGet source - name: Add NuGet source
@ -38,6 +39,9 @@ jobs:
- name: Test .NET 6 - name: Test .NET 6
run: dotnet test --no-build --verbosity normal --configuration Release --framework net6.0 --collect:"XPlat Code Coverage" --results-directory test-results/net6.0 run: dotnet test --no-build --verbosity normal --configuration Release --framework net6.0 --collect:"XPlat Code Coverage" --results-directory test-results/net6.0
- name: Test .NET 7
run: dotnet test --no-build --verbosity normal --configuration Release --framework net7.0 --collect:"XPlat Code Coverage" --results-directory test-results/net7.0
- name: Test .NET 8 - name: Test .NET 8
run: dotnet test --no-build --verbosity normal --configuration Release --framework net8.0 --collect:"XPlat Code Coverage" --results-directory test-results/net8.0 run: dotnet test --no-build --verbosity normal --configuration Release --framework net8.0 --collect:"XPlat Code Coverage" --results-directory test-results/net8.0

View File

@ -23,7 +23,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- X10D: Added `TextWriter.WriteLineNoAlloc(uint[, ReadOnlySpan<char>[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteLineNoAlloc(uint[, ReadOnlySpan<char>[, IFormatProvider]])`.
- X10D: Added `TextWriter.WriteLineNoAlloc(long[, ReadOnlySpan<char>[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteLineNoAlloc(long[, ReadOnlySpan<char>[, IFormatProvider]])`.
- X10D: Added `TextWriter.WriteLineNoAlloc(ulong[, ReadOnlySpan<char>[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteLineNoAlloc(ulong[, ReadOnlySpan<char>[, IFormatProvider]])`.
- X10D: Added `Range.GetEnumerator` (and `RangeEnumerator`), implementing Python-esque `for` loops in C#.
- X10D: Added `string.ConcatIf`. - X10D: Added `string.ConcatIf`.
- X10D: Added `string.MDBold`, `string.MDCode`, `string.MDCodeBlock([string])`, `string.MDHeading(int)`, - X10D: Added `string.MDBold`, `string.MDCode`, `string.MDCodeBlock([string])`, `string.MDHeading(int)`,
`string.MDItalic`, `string.MDLink`, `string.MDStrikeOut`, and `string.MDUnderline` for Markdown formatting. `string.MDItalic`, `string.MDLink`, `string.MDStrikeOut`, and `string.MDUnderline` for Markdown formatting.
@ -40,7 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
BigEndian/LittleEndian methods. BigEndian/LittleEndian methods.
- X10D: `Stream.GetHash<>` and `Stream.TryWriteHash<>` now throw ArgumentException in lieu of - X10D: `Stream.GetHash<>` and `Stream.TryWriteHash<>` now throw ArgumentException in lieu of
TypeInitializationException. TypeInitializationException.
- X10D: `char.IsEmoji` no longer allocates for .NET 7+. - X10D: `char.IsEmoji` no longer allocates for .NET 7.
- X10D: `string.Repeat` is now more efficient. - X10D: `string.Repeat` is now more efficient.
### Removed ### Removed
@ -49,7 +48,6 @@ TypeInitializationException.
- X10D: Removed `Endianness` enum. - X10D: Removed `Endianness` enum.
- X10D: Removed `Span<T>.Replace(T, T)` for .NET 8 target. - X10D: Removed `Span<T>.Replace(T, T)` for .NET 8 target.
- X10D: Removed .NET Standard 2.1 target. - X10D: Removed .NET Standard 2.1 target.
- X10D: Removed extensions for `Progress<T>`. These are already provided by [`Observable.FromEventPattern`](https://learn.microsoft.com/en-us/previous-versions/dotnet/reactive-extensions/hh229424(v=vs.103)).
- X10D.Hosting: Removed .NET Standard 2.1 target. - X10D.Hosting: Removed .NET Standard 2.1 target.
- X10D.DSharpPlus: Complete sunset of library. This library will not be updated to support DSharpPlus v5.0.0 (#83). - X10D.DSharpPlus: Complete sunset of library. This library will not be updated to support DSharpPlus v5.0.0 (#83).
- X10D.Unity: Complete sunset of library. This library will not be updated effective immediately (#86). - X10D.Unity: Complete sunset of library. This library will not be updated effective immediately (#86).
@ -58,7 +56,6 @@ TypeInitializationException.
### Fixed ### Fixed
- X10D: Fixed `decimal.TryWriteBigEndianBytes` and `decimal.TryWriteLittleEndianBytes`.
- X10D.Hosting: Fixed `AddHostedSingleton` not accepting an interface as the service type. - X10D.Hosting: Fixed `AddHostedSingleton` not accepting an interface as the service type.
## [3.3.0] - 2023-08-21 ## [3.3.0] - 2023-08-21

View File

@ -5,7 +5,7 @@ or submit a pull request.
### Pull request guidelines ### Pull request guidelines
This project uses C# 12.0 language features where feasible, and adheres to StyleCop rules with some minor adjustments. This project uses C# 11.0 language features where feasible, and adheres to StyleCop rules with some minor adjustments.
There is an `.editorconfig` included in this repository. For quick and painless pull requests, ensure that the analyzer does not There is an `.editorconfig` included in this repository. For quick and painless pull requests, ensure that the analyzer does not
throw warnings. throw warnings.
@ -17,7 +17,7 @@ convetional commits specification.
Below are a few pointers to which you may refer, but keep in mind this is not an exhaustive list: Below are a few pointers to which you may refer, but keep in mind this is not an exhaustive list:
- Use C# 12.0 features where possible - Use C# 11.0 features where possible
- Try to ensure code is CLS-compliant. Where this is not possible, decorate methods with `CLSCompliantAttribute` and pass `false` - Try to ensure code is CLS-compliant. Where this is not possible, decorate methods with `CLSCompliantAttribute` and pass `false`
- Follow all .NET guidelines and coding conventions. - Follow all .NET guidelines and coding conventions.
See https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions See https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions

View File

@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<LangVersion>12.0</LangVersion> <LangVersion>11.0</LangVersion>
<Optimize>true</Optimize> <Optimize>true</Optimize>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net8.0;net6.0</TargetFrameworks> <TargetFrameworks>net8.0;net7.0;net6.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net8.0;net6.0</TargetFrameworks> <TargetFrameworks>net8.0;net7.0;net6.0</TargetFrameworks>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<CoverletOutputFormat>xml,cobertura</CoverletOutputFormat> <CoverletOutputFormat>xml,cobertura</CoverletOutputFormat>

View File

@ -1,66 +0,0 @@
using NUnit.Framework;
using X10D.Core;
namespace X10D.Tests.Core;
[TestFixture]
internal class RangeTests
{
[Test]
public void Range_GetEnumerator_ShouldReturnRangeEnumerator()
{
Assert.Multiple(() =>
{
Assert.That(5..10, Is.TypeOf<Range>());
Assert.That((5..10).GetEnumerator(), Is.TypeOf<RangeEnumerator>());
});
}
[Test]
public void Loop_OverRange0To10_ShouldCountFrom0To10Inclusive()
{
int value = 0;
foreach (int i in 0..10)
{
Assert.That(i, Is.EqualTo(value));
value++;
}
}
[Test]
public void Loop_OverRangeNegative5To5_ShouldCountFromNegative5To5Inclusive()
{
int value = -5;
foreach (int i in ^5..5)
{
Assert.That(i, Is.EqualTo(value));
value++;
}
}
[Test]
public void Loop_OverRange5ToNegative5_ShouldCountFrom5ToNegative5Inclusive()
{
int value = 5;
foreach (int i in 5..^5)
{
Assert.That(i, Is.EqualTo(value));
value--;
}
}
[Test]
public void Loop_OverRange10To0_ShouldCountFrom10To0Inclusive()
{
int value = 10;
foreach (int i in 10..0)
{
Assert.That(i, Is.EqualTo(value));
value--;
}
}
}

View File

@ -1,72 +0,0 @@
using NUnit.Framework;
using X10D.IO;
namespace X10D.Tests.IO;
[TestFixture]
internal class DecimalTests
{
[Test]
public void GetBigEndianBytes_ShouldReturnBytes_InBigEndian()
{
const decimal value = 1234m;
byte[] expected = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 210];
byte[] bytes = value.GetBigEndianBytes();
CollectionAssert.AreEqual(expected, bytes);
}
[Test]
public void GetLittleEndianBytes_ShouldReturnBytes_InLittleEndian()
{
const decimal value = 1234m;
byte[] expected = [210, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
byte[] bytes = value.GetLittleEndianBytes();
CollectionAssert.AreEqual(expected, bytes);
}
[Test]
public void TryWriteBigEndianBytes_ShouldWriteBytes_InBigEndian()
{
const decimal value = 1234m;
byte[] expected = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 210];
Span<byte> bytes = stackalloc byte[16];
Assert.That(value.TryWriteBigEndianBytes(bytes));
CollectionAssert.AreEqual(expected, bytes.ToArray());
}
[Test]
public void TryWriteLittleEndianBytes_ShouldWriteBytes_InLittleEndian()
{
const decimal value = 1234m;
byte[] expected = [210, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
Span<byte> bytes = stackalloc byte[16];
Assert.That(value.TryWriteLittleEndianBytes(bytes));
CollectionAssert.AreEqual(expected, bytes.ToArray());
}
[Test]
public void TryWriteBigEndianBytes_ShouldReturnFalse_GivenSmallSpan()
{
const decimal value = 1234m;
Span<byte> bytes = Span<byte>.Empty;
Assert.That(value.TryWriteBigEndianBytes(bytes), Is.False);
}
[Test]
public void TryWriteLittleEndianBytes_ShouldReturnFalse_GivenSmallSpan()
{
const decimal value = 1234m;
Span<byte> bytes = Span<byte>.Empty;
Assert.That(value.TryWriteLittleEndianBytes(bytes), Is.False);
}
}

View File

@ -48,10 +48,9 @@ internal partial class StreamTests
Span<byte> actual = stackalloc byte[16]; Span<byte> actual = stackalloc byte[16];
ReadOnlySpan<byte> expected = stackalloc byte[] ReadOnlySpan<byte> expected = stackalloc byte[]
{ {
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x68 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x10, 0x00, 0x00
}; };
int read = stream.Read(actual); int read = stream.Read(actual);
Trace.WriteLine(string.Join(' ', actual.ToArray()));
Assert.That(read, Is.EqualTo(16)); Assert.That(read, Is.EqualTo(16));
CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray());

View File

@ -1,32 +0,0 @@
#if NET7_0_OR_GREATER
using NUnit.Framework;
using X10D.Math;
namespace X10D.Tests.Numerics;
[TestFixture]
internal class NumberTests
{
[Test]
public void Sign_ShouldReturn1_GivenPositiveNumber()
{
Assert.That(NumberExtensions.Sign(2), Is.Positive);
Assert.That(NumberExtensions.Sign(2), Is.EqualTo(1));
}
[Test]
public void Sign_Should0_GivenZero()
{
Assert.That(NumberExtensions.Sign(0), Is.Not.Positive);
Assert.That(NumberExtensions.Sign(0), Is.Not.Negative);
Assert.That(NumberExtensions.Sign(0), Is.EqualTo(0));
}
[Test]
public void Sign_ShouldReturnNegative1_GivenNegativeNumber()
{
Assert.That(NumberExtensions.Sign(-2), Is.Negative);
Assert.That(NumberExtensions.Sign(-2), Is.EqualTo(-1));
}
}
#endif

View File

@ -0,0 +1,67 @@
using NUnit.Framework;
using X10D.Reactive;
namespace X10D.Tests.Reactive;
[TestFixture]
internal class ProgressTests
{
[Test]
public void OnProgressChanged_ShouldCallCompletionDelegate_GivenCompletionValue()
{
var subscriberWasCalled = false;
var completionWasCalled = false;
var progress = new Progress<float>();
progress.OnProgressChanged(1.0f).Subscribe(_ => subscriberWasCalled = true, () => completionWasCalled = true);
((IProgress<float>)progress).Report(0.5f);
((IProgress<float>)progress).Report(1.0f);
Thread.Sleep(1000);
Assert.That(subscriberWasCalled);
Assert.That(completionWasCalled);
}
[Test]
public void OnProgressChanged_ShouldCallSubscribers_OnProgressChanged()
{
var subscriberWasCalled = false;
var progress = new Progress<float>();
progress.OnProgressChanged().Subscribe(_ => subscriberWasCalled = true);
((IProgress<float>)progress).Report(0.5f);
Thread.Sleep(1000);
Assert.That(subscriberWasCalled);
}
[Test]
public void OnProgressChanged_ShouldCallSubscribers_OnProgressChanged_GivenCompletionValue()
{
var subscriberWasCalled = false;
var progress = new Progress<float>();
progress.OnProgressChanged(1.0f).Subscribe(_ => subscriberWasCalled = true);
((IProgress<float>)progress).Report(0.5f);
Thread.Sleep(1000);
Assert.That(subscriberWasCalled);
}
[Test]
public void OnProgressChanged_ShouldThrowArgumentNullException_GivenNullProgress()
{
Progress<float> progress = null!;
Assert.Throws<ArgumentNullException>(() => progress.OnProgressChanged());
}
[Test]
public void OnProgressChanged_ShouldThrowArgumentNullException_GivenNullProgressAndCompletionValue()
{
Progress<float> progress = null!;
Assert.Throws<ArgumentNullException>(() => progress.OnProgressChanged(1.0f));
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net8.0;net6.0</TargetFrameworks> <TargetFrameworks>net8.0;net7.0;net6.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,5 +1,4 @@
#if NET7_0_OR_GREATER #if NET7_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -49,28 +48,6 @@ public static class BinaryIntegerExtensions
} }
} }
UnpackInternal(value, destination);
}
[MethodImpl(CompilerResources.MaxOptimization)]
private static void UnpackInternal_Fallback<TInteger>(this TInteger value, Span<bool> destination)
where TInteger : unmanaged, IBinaryInteger<TInteger>
{
unsafe
{
int bitCount = sizeof(TInteger) * 8;
for (var index = 0; index < bitCount; index++)
{
destination[index] = (value & (TInteger.One << index)) != TInteger.Zero;
}
}
}
[ExcludeFromCodeCoverage]
[MethodImpl(CompilerResources.MaxOptimization)]
private static void UnpackInternal<TInteger>(TInteger value, Span<bool> destination)
where TInteger : unmanaged, IBinaryInteger<TInteger>
{
switch (value) switch (value)
{ {
case byte valueByte when Sse3.IsSupported: case byte valueByte when Sse3.IsSupported:
@ -94,5 +71,19 @@ public static class BinaryIntegerExtensions
break; break;
} }
} }
[MethodImpl(CompilerResources.MaxOptimization)]
internal static void UnpackInternal_Fallback<TInteger>(this TInteger value, Span<bool> destination)
where TInteger : unmanaged, IBinaryInteger<TInteger>
{
unsafe
{
int bitCount = sizeof(TInteger) * 8;
for (var index = 0; index < bitCount; index++)
{
destination[index] = (value & (TInteger.One << index)) != TInteger.Zero;
}
}
}
} }
#endif #endif

View File

@ -1,53 +0,0 @@
namespace X10D.Core;
/// <summary>
/// Enumerates the indices of a <see cref="Range" />.
/// </summary>
public struct RangeEnumerator
{
private readonly bool _decrement;
private readonly int _endValue;
/// <summary>
/// Initializes a new instance of the <see cref="RangeEnumerator" /> structure.
/// </summary>
/// <param name="range">The range over which to enumerate.</param>
public RangeEnumerator(Range range)
{
Index start = range.Start;
Index end = range.End;
int startValue = start.IsFromEnd ? -start.Value : start.Value;
_endValue = end.IsFromEnd ? -end.Value : end.Value;
_decrement = _endValue < startValue;
Current = _decrement ? startValue + 1 : startValue - 1;
}
/// <summary>
/// Gets the element in the collection at the current position of the enumerator.
/// </summary>
/// <value>The element in the collection at the current position of the enumerator.</value>
public int Current { get; private set; }
/// <summary>
/// Advances the enumerator to the next element of the collection.
/// </summary>
/// <returns>
/// <see langword="true" /> if the enumerator was successfully advanced to the next element; <see langword="false" /> if
/// the enumerator has passed the end of the collection.
/// </returns>
public bool MoveNext()
{
int value = Current;
if (_decrement)
{
Current--;
return value > _endValue;
}
Current++;
return value < _endValue;
}
}

View File

@ -1,48 +0,0 @@
namespace X10D.Core;
/// <summary>
/// Extension methods for <see cref="Range" />.
/// </summary>
public static class RangeExtensions
{
/// <summary>
/// Allows the ability to use a <c>for</c> loop to iterate over the indices of a <see cref="Range" />. The indices of the
/// range are the inclusive lower and upper bounds of the enumeration.
/// </summary>
/// <param name="range">The range whose indices over which will be enumerated.</param>
/// <returns>A <see cref="RangeEnumerator" /> that will enumerate over the indices of <paramref name="range" />.</returns>
/// <remarks>
/// This method aims to implement Python-esque for loops in C# by taking advantage of the language syntax used to define
/// a <see cref="Range" /> value. Negative bounds may be specified using the C# <c>^</c> operator, or by providing an
/// <see cref="Index" /> whose <see cref="Index.IsFromEnd" /> property is <see langword="true" />.
/// </remarks>
/// <example>
/// The following example counts from 0 to 10 inclusive:
/// <code>
/// foreach (var i in 0..10)
/// {
/// Console.WriteLine(i);
/// }
/// </code>
///
/// To use negative bounds, use the <c>^</c> operator. The following example counts from -5 to 5 inclusive:
/// <code>
/// foreach (var i in ^5..5)
/// {
/// Console.WriteLine(i);
/// }
/// </code>
///
/// Decrementing enumeration is supported. The following example counts from 5 to -5 inclusive:
/// <code>
/// foreach (var i in 5..^5)
/// {
/// Console.WriteLine(i);
/// }
/// </code>
/// </example>
public static RangeEnumerator GetEnumerator(this Range range)
{
return new RangeEnumerator(range);
}
}

View File

@ -92,12 +92,14 @@ public static class SpanExtensions
} }
// dotcover disable // dotcover disable
//NOSONAR
default: default:
#if NET7_0_OR_GREATER #if NET7_0_OR_GREATER
throw new UnreachableException(string.Format(ExceptionMessages.EnumSizeIsUnexpected, Unsafe.SizeOf<T>())); throw new UnreachableException(string.Format(ExceptionMessages.EnumSizeIsUnexpected, Unsafe.SizeOf<T>()));
#else #else
throw new ArgumentException(string.Format(ExceptionMessages.EnumSizeIsUnexpected, Unsafe.SizeOf<T>())); throw new ArgumentException(string.Format(ExceptionMessages.EnumSizeIsUnexpected, Unsafe.SizeOf<T>()));
#endif #endif
//NOSONAR
// dotcover enable // dotcover enable
} }
} }

View File

@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -12,11 +13,11 @@ public static class DecimalExtensions
/// Converts the current decimal number into an array of bytes, as little endian. /// Converts the current decimal number into an array of bytes, as little endian.
/// </summary> /// </summary>
/// <param name="value">The <see cref="int" /> value.</param> /// <param name="value">The <see cref="int" /> value.</param>
/// <returns>An array of bytes with length 16.</returns> /// <returns>An array of bytes with length 4.</returns>
[Pure] [Pure]
public static byte[] GetBigEndianBytes(this decimal value) public static byte[] GetBigEndianBytes(this decimal value)
{ {
Span<byte> buffer = stackalloc byte[16]; Span<byte> buffer = stackalloc byte[4];
value.TryWriteBigEndianBytes(buffer); value.TryWriteBigEndianBytes(buffer);
return buffer.ToArray(); return buffer.ToArray();
} }
@ -25,11 +26,11 @@ public static class DecimalExtensions
/// Converts the current decimal number into an array of bytes, as little endian. /// Converts the current decimal number into an array of bytes, as little endian.
/// </summary> /// </summary>
/// <param name="value">The <see cref="int" /> value.</param> /// <param name="value">The <see cref="int" /> value.</param>
/// <returns>An array of bytes with length 16.</returns> /// <returns>An array of bytes with length 4.</returns>
[Pure] [Pure]
public static byte[] GetLittleEndianBytes(this decimal value) public static byte[] GetLittleEndianBytes(this decimal value)
{ {
Span<byte> buffer = stackalloc byte[16]; Span<byte> buffer = stackalloc byte[4];
value.TryWriteLittleEndianBytes(buffer); value.TryWriteLittleEndianBytes(buffer);
return buffer.ToArray(); return buffer.ToArray();
} }
@ -43,17 +44,23 @@ public static class DecimalExtensions
public static bool TryWriteBigEndianBytes(this decimal value, Span<byte> destination) public static bool TryWriteBigEndianBytes(this decimal value, Span<byte> destination)
{ {
Span<int> buffer = stackalloc int[4]; Span<int> buffer = stackalloc int[4];
decimal.GetBits(value, buffer); GetBits(value, buffer);
Span<byte> result = stackalloc byte[16]; if (buffer[0].TryWriteBigEndianBytes(destination[..4]) &&
MemoryMarshal.Cast<int, byte>(buffer).CopyTo(result); buffer[1].TryWriteBigEndianBytes(destination[4..8]) &&
buffer[2].TryWriteBigEndianBytes(destination[8..12]) &&
if (BitConverter.IsLittleEndian) buffer[3].TryWriteBigEndianBytes(destination[12..]))
{ {
result.Reverse(); if (BitConverter.IsLittleEndian)
{
destination.Reverse();
}
return true;
} }
return result.TryCopyTo(destination); destination.Clear();
return false;
} }
/// <summary> /// <summary>
@ -65,16 +72,44 @@ public static class DecimalExtensions
public static bool TryWriteLittleEndianBytes(this decimal value, Span<byte> destination) public static bool TryWriteLittleEndianBytes(this decimal value, Span<byte> destination)
{ {
Span<int> buffer = stackalloc int[4]; Span<int> buffer = stackalloc int[4];
decimal.GetBits(value, buffer); GetBits(value, buffer);
Span<byte> result = stackalloc byte[16]; if (buffer[0].TryWriteLittleEndianBytes(destination[..4]) &&
MemoryMarshal.Cast<int, byte>(buffer).CopyTo(result); buffer[1].TryWriteLittleEndianBytes(destination[4..8]) &&
buffer[2].TryWriteLittleEndianBytes(destination[8..12]) &&
if (!BitConverter.IsLittleEndian) buffer[3].TryWriteLittleEndianBytes(destination[12..]))
{ {
result.Reverse(); if (!BitConverter.IsLittleEndian)
{
destination.Reverse();
}
return true;
} }
return result.TryCopyTo(destination); destination.Clear();
return false;
} }
private static void GetBits(decimal value, Span<int> destination)
{
_ = decimal.GetBits(value, destination);
}
#if !NET5_0_OR_GREATER
private static void WriteBits(Span<int> destination, Span<byte> buffer)
{
var flags = MemoryMarshal.Read<int>(buffer[..4]);
var hi = MemoryMarshal.Read<int>(buffer[4..8]);
var lo = MemoryMarshal.Read<long>(buffer[8..]);
var low = (uint)lo;
var mid = (uint)(lo >> 32);
destination[0] = (int)low;
destination[1] = (int)mid;
destination[2] = hi;
destination[3] = flags;
}
#endif
} }

View File

@ -0,0 +1,33 @@
namespace X10D.Reactive;
/// <summary>
/// Represents a disposable that removes an observer from a collection of observers.
/// </summary>
internal readonly struct ObservableDisposer<T> : IDisposable
{
private readonly HashSet<IObserver<T>> _observers;
private readonly IObserver<T> _observer;
private readonly Action? _additionalAction;
/// <summary>
/// Initializes a new instance of the <see cref="ObservableDisposer{T}" /> struct.
/// </summary>
/// <param name="observers">A collection of observers from which to remove the specified observer.</param>
/// <param name="observer">The observer to remove from the collection.</param>
/// <param name="additionalAction">The additional action to run on dispose.</param>
public ObservableDisposer(HashSet<IObserver<T>> observers, IObserver<T> observer, Action? additionalAction)
{
_observers = observers ?? throw new ArgumentNullException(nameof(observers));
_observer = observer ?? throw new ArgumentNullException(nameof(observer));
_additionalAction = additionalAction;
}
/// <summary>
/// Removes the observer from the collection of observers.
/// </summary>
public void Dispose()
{
_observers.Remove(_observer);
_additionalAction?.Invoke();
}
}

View File

@ -0,0 +1,89 @@
namespace X10D.Reactive;
/// <summary>
/// Provides extension methods for <see cref="Progress{T}" />.
/// </summary>
public static class ProgressExtensions
{
/// <summary>
/// Wraps the <see cref="Progress{T}.ProgressChanged" /> event of the current <see cref="Progress{T}" /> in an
/// <see cref="IObservable{T}" /> object.
/// </summary>
/// <param name="progress">The progress whose <see cref="Progress{T}.ProgressChanged" /> event to wrap.</param>
/// <typeparam name="T">The type of progress update value.</typeparam>
/// <returns>
/// An <see cref="IObservable{T}" /> object that wraps the <see cref="Progress{T}.ProgressChanged" /> event of the current
/// <see cref="Progress{T}" />.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="progress" /> is <see langword="null" />.</exception>
public static IObservable<T> OnProgressChanged<T>(this Progress<T> progress)
{
if (progress is null)
{
throw new ArgumentNullException(nameof(progress));
}
var progressObservable = new ProgressObservable<T>();
void ProgressChangedHandler(object? sender, T args)
{
IObserver<T>[] observers = progressObservable.Observers;
for (var index = 0; index < observers.Length; index++)
{
observers[index].OnNext(args);
}
}
progress.ProgressChanged += ProgressChangedHandler;
progressObservable.OnDispose = () => progress.ProgressChanged -= ProgressChangedHandler;
return progressObservable;
}
/// <summary>
/// Wraps the <see cref="Progress{T}.ProgressChanged" /> event of the current <see cref="Progress{T}" /> in an
/// <see cref="IObservable{T}" /> object, and completes the observable when the progress reaches the specified value.
/// </summary>
/// <param name="progress">The progress whose <see cref="Progress{T}.ProgressChanged" /> event to wrap.</param>
/// <param name="completeValue">The value that indicates completion.</param>
/// <typeparam name="T">The type of progress update value.</typeparam>
/// <returns>
/// An <see cref="IObservable{T}" /> object that wraps the <see cref="Progress{T}.ProgressChanged" /> event of the current
/// <see cref="Progress{T}" />.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="progress" /> is <see langword="null" />.</exception>
public static IObservable<T> OnProgressChanged<T>(this Progress<T> progress, T completeValue)
{
if (progress is null)
{
throw new ArgumentNullException(nameof(progress));
}
var progressObservable = new ProgressObservable<T>();
var comparer = EqualityComparer<T>.Default;
void ProgressChangedHandler(object? sender, T args)
{
IObserver<T>[] observers = progressObservable.Observers;
for (var index = 0; index < observers.Length; index++)
{
observers[index].OnNext(args);
}
if (comparer.Equals(args, completeValue))
{
for (var index = 0; index < observers.Length; index++)
{
observers[index].OnCompleted();
}
}
}
progress.ProgressChanged += ProgressChangedHandler;
progressObservable.OnDispose = () => progress.ProgressChanged -= ProgressChangedHandler;
return progressObservable;
}
}

View File

@ -0,0 +1,36 @@
namespace X10D.Reactive;
/// <summary>
/// Represents a concrete implementation of <see cref="IObservable{T}" /> that tracks progress of a <see cref="Progress{T}"/>.
/// </summary>
internal sealed class ProgressObservable<T> : IObservable<T>
{
private readonly HashSet<IObserver<T>> _observers = new();
/// <summary>
/// Gets the observers.
/// </summary>
/// <value>The observers.</value>
public IObserver<T>[] Observers
{
get => _observers.ToArray();
}
internal Action? OnDispose { get; set; }
/// <summary>
/// Subscribes the specified observer to the progress tracker.
/// </summary>
/// <param name="observer">The observer.</param>
/// <returns>An object which can be disposed to unsubscribe from progress tracking.</returns>
public IDisposable Subscribe(IObserver<T> observer)
{
if (observer is null)
{
throw new ArgumentNullException(nameof(observer));
}
_observers.Add(observer);
return new ObservableDisposer<T>(_observers, observer, OnDispose);
}
}

View File

@ -98,6 +98,7 @@ public static class RuneExtensions
} }
// dotcover disable // dotcover disable
//NOSONAR
default: default:
string exceptionFormat = ExceptionMessages.UnexpectedRuneUtf8SequenceLength; string exceptionFormat = ExceptionMessages.UnexpectedRuneUtf8SequenceLength;
string message = string.Format(CultureInfo.CurrentCulture, exceptionFormat, length); string message = string.Format(CultureInfo.CurrentCulture, exceptionFormat, length);
@ -106,6 +107,7 @@ public static class RuneExtensions
#else #else
throw new InvalidOperationException(message); throw new InvalidOperationException(message);
#endif #endif
//NOSONAR
// dotcover enable // dotcover enable
} }
} }

View File

@ -1,2 +0,0 @@
ignore:
- "X10D/src/ExceptionMessages.Designer.cs"

View File

@ -3,9 +3,6 @@
X10D 4.0.0 is a major release that introduces breaking changes. This document will help you migrate your code from 3.x.x X10D 4.0.0 is a major release that introduces breaking changes. This document will help you migrate your code from 3.x.x
to 4.0.0. to 4.0.0.
When a breaking change is mentioned, the compatibility mirrors that of the Microsoft documentation for .NET, which you
can find [here](https://learn.microsoft.com/en-us/dotnet/core/compatibility/categories).
## Removed APIs ## Removed APIs
### X10D.DSharpPlus library ### X10D.DSharpPlus library
@ -15,18 +12,8 @@ wrapper library. However, I have since moved to using a different library, and a
scope of X10D or in my best interest to maintain it. The library will remain available on NuGet until DSharpPlus release scope of X10D or in my best interest to maintain it. The library will remain available on NuGet until DSharpPlus release
5.0.0 as stable, and X10D.DSharpPlus will NOT be part of X10D 4.0.0. I'm sorry for any inconvenience this may cause. 5.0.0 as stable, and X10D.DSharpPlus will NOT be part of X10D 4.0.0. I'm sorry for any inconvenience this may cause.
### X10D.Unity library
The `X10D.Unity` library has been removed. This library was used to provide extension methods for the Unity API. Due to
game development politics, I no longer feel it in my best interest to continue development of the Unity package. The
library will remain on NuGet for the foreseeable future but will no longer be maintained. The `upm` branch of the Git
repository will remain available indefinitely also.
### `Endianness` enum ### `Endianness` enum
**Source incompatible change**
The `Endianness` enum was used to specify the endianness of data when reading or writing to a stream. This was causing The `Endianness` enum was used to specify the endianness of data when reading or writing to a stream. This was causing
some clutter, and makes it harder to develop X10D, so it was removed. In its stead, any method which accepted an some clutter, and makes it harder to develop X10D, so it was removed. In its stead, any method which accepted an
`Endianness` parameter now has two overloads: one for big-endian, and one for little-endian. For example, the following `Endianness` parameter now has two overloads: one for big-endian, and one for little-endian. For example, the following
@ -54,16 +41,12 @@ Span<byte> buffer = stackalloc byte[4];
### `IEnumerable<T>.ConcatOne(T)` extension method ### `IEnumerable<T>.ConcatOne(T)` extension method
**Source incompatible change**
The `IEnumerable<T>.ConcatOne` extension method was used to concatenate a single item to an enumerable. At the time, I The `IEnumerable<T>.ConcatOne` extension method was used to concatenate a single item to an enumerable. At the time, I
was unaware of the `Enumerable.Append` method, which does the same thing. As such, `ConcatOne` has been removed. There was unaware of the `Enumerable.Append` method, which does the same thing. As such, `ConcatOne` has been removed. There
is no migration path for this, as the built in `Append` method from LINQ is a drop-in replacement. is no migration path for this, as the built in `Append` method from LINQ is a drop-in replacement.
## Exception Changes ## Exception Changes
**Source incompatible change**
If you were previously catching TypeInitializationException when calling `Stream.GetHash<>` or `Stream.TryWriteHash<>`, If you were previously catching TypeInitializationException when calling `Stream.GetHash<>` or `Stream.TryWriteHash<>`,
you will now need to catch a ArgumentException instead. The justification for this change is that ArgumentException is you will now need to catch a ArgumentException instead. The justification for this change is that ArgumentException is
more general, and more easily understood by developers. more general, and more easily understood by developers.

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net8.0;net6.0</TargetFrameworks> <TargetFrameworks>net8.0;net7.0;net6.0</TargetFrameworks>
<Configuration>Release</Configuration> <Configuration>Release</Configuration>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Optimize>true</Optimize> <Optimize>true</Optimize>