Merge branch 'develop' into main

This commit is contained in:
Oliver Booth 2023-04-10 13:54:05 +01:00
commit b70b704b63
No known key found for this signature in database
GPG Key ID: 20BEB9DC87961025
13 changed files with 416 additions and 26 deletions

View File

@ -8,10 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 4.0.0 - [Unreleased] ## 4.0.0 - [Unreleased]
### Added ### Added
- X10D: Added extension methods for `DateOnly`, for parity with `DateTime` and `DateTimeOffset`. - X10D: Added extension methods for `DateOnly`, for parity with `DateTime` and `DateTimeOffset`.
- X10D: Added math-related extension methods for `BigInteger`. - X10D: Added math-related extension methods for `BigInteger`.
- X10D: Added `Span<T>.Replace(T, T)`. - X10D: Added `Span<T>.Replace(T, T)`.
- X10D: Added `CountDigits` for integer types. - X10D: Added `CountDigits` for integer types.
- X10D: Added `Progress<T>.OnProgressChanged([T])`;
- X10D: Added `TextWriter.WriteNoAlloc(int[, ReadOnlySpan<char>[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteNoAlloc(int[, ReadOnlySpan<char>[, IFormatProvider]])`.
- X10D: Added `TextWriter.WriteNoAlloc(uint[, ReadOnlySpan<char>[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteNoAlloc(uint[, ReadOnlySpan<char>[, IFormatProvider]])`.
- X10D: Added `TextWriter.WriteNoAlloc(long[, ReadOnlySpan<char>[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteNoAlloc(long[, ReadOnlySpan<char>[, IFormatProvider]])`.
@ -22,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- X10D: Added `TextWriter.WriteLineNoAlloc(ulong[, ReadOnlySpan<char>[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteLineNoAlloc(ulong[, ReadOnlySpan<char>[, IFormatProvider]])`.
### Changed ### Changed
- X10D: `DateTime.Age(DateTime)` and `DateTimeOffset.Age(DateTimeOffset)` parameter renamed from `asOf` to `referenceDate`. - X10D: `DateTime.Age(DateTime)` and `DateTimeOffset.Age(DateTimeOffset)` parameter renamed from `asOf` to `referenceDate`.
## [3.2.0] - 2023-04-03 ## [3.2.0] - 2023-04-03

View File

@ -22,6 +22,7 @@
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2"/> <PackageReference Include="NUnit3TestAdapter" Version="4.4.2"/>
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/> <PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
<PackageReference Include="coverlet.collector" Version="3.2.0"/> <PackageReference Include="coverlet.collector" Version="3.2.0"/>
<PackageReference Include="System.Reactive" Version="5.0.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,67 @@
using NUnit.Framework;
using X10D.Reactive;
namespace X10D.Tests.Reactive;
[TestFixture]
public 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

@ -4,13 +4,14 @@ using System.Collections;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Object = UnityEngine.Object;
namespace X10D.Unity.Tests namespace X10D.Unity.Tests
{ {
public class ComponentTests public class ComponentTests
{ {
[UnityTest] [Test]
public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent() public void GetComponentsInChildrenOnly_ShouldIgnoreParent()
{ {
var parent = new GameObject(); var parent = new GameObject();
var rigidbody = parent.AddComponent<Rigidbody>(); var rigidbody = parent.AddComponent<Rigidbody>();
@ -20,10 +21,11 @@ namespace X10D.Unity.Tests
child.AddComponent<Rigidbody>(); child.AddComponent<Rigidbody>();
Rigidbody[] components = rigidbody.GetComponentsInChildrenOnly<Rigidbody>(); Rigidbody[] components = rigidbody.GetComponentsInChildrenOnly<Rigidbody>();
Assert.That(components.Length, Is.EqualTo(1)); Assert.That(components, Has.Length.EqualTo(1));
Assert.That(child, Is.EqualTo(components[0].gameObject)); Assert.That(child, Is.EqualTo(components[0].gameObject));
yield break; Object.Destroy(parent);
Object.Destroy(child);
} }
} }
} }

View File

@ -1,16 +1,16 @@
#nullable enable #nullable enable
using System.Collections; using System.Diagnostics.CodeAnalysis;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using Object = UnityEngine.Object;
namespace X10D.Unity.Tests namespace X10D.Unity.Tests
{ {
public class GameObjectTests public class GameObjectTests
{ {
[UnityTest] [Test]
public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent() public void GetComponentsInChildrenOnly_ShouldIgnoreParent()
{ {
var parent = new GameObject(); var parent = new GameObject();
parent.AddComponent<Rigidbody>(); parent.AddComponent<Rigidbody>();
@ -20,14 +20,16 @@ namespace X10D.Unity.Tests
child.AddComponent<Rigidbody>(); child.AddComponent<Rigidbody>();
Rigidbody[] components = parent.GetComponentsInChildrenOnly<Rigidbody>(); Rigidbody[] components = parent.GetComponentsInChildrenOnly<Rigidbody>();
Assert.That(components.Length, Is.EqualTo(1)); Assert.That(components, Has.Length.EqualTo(1));
Assert.That(child, Is.EqualTo(components[0].gameObject)); Assert.That(child, Is.EqualTo(components[0].gameObject));
yield break; Object.Destroy(parent);
Object.Destroy(child);
} }
[UnityTest] [Test]
public IEnumerator LookAt_ShouldRotateSameAsTransform() [SuppressMessage("ReSharper", "Unity.InefficientPropertyAccess", Justification = "False positive.")]
public void LookAt_ShouldRotateSameAsTransform()
{ {
var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}}; var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}};
var second = new GameObject {transform = {position = Vector3.right, rotation = Quaternion.identity}}; var second = new GameObject {transform = {position = Vector3.right, rotation = Quaternion.identity}};
@ -58,11 +60,12 @@ namespace X10D.Unity.Tests
first.LookAt(Vector3.right); first.LookAt(Vector3.right);
Assert.That(firstTransform.rotation, Is.EqualTo(expected)); Assert.That(firstTransform.rotation, Is.EqualTo(expected));
yield break; Object.Destroy(first);
Object.Destroy(second);
} }
[UnityTest] [Test]
public IEnumerator SetLayerRecursively_ShouldSetLayerRecursively() public void SetLayerRecursively_ShouldSetLayerRecursively()
{ {
var parent = new GameObject(); var parent = new GameObject();
var child = new GameObject(); var child = new GameObject();
@ -82,11 +85,14 @@ namespace X10D.Unity.Tests
Assert.That(child.layer, Is.EqualTo(layer)); Assert.That(child.layer, Is.EqualTo(layer));
Assert.That(grandChild.layer, Is.EqualTo(layer)); Assert.That(grandChild.layer, Is.EqualTo(layer));
yield break; Object.Destroy(parent);
Object.Destroy(child);
Object.Destroy(grandChild);
} }
[UnityTest] [Test]
public IEnumerator SetParent_ShouldSetParent() [SuppressMessage("ReSharper", "Unity.InefficientPropertyAccess", Justification = "False positive.")]
public void SetParent_ShouldSetParent()
{ {
var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}}; var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}};
var second = new GameObject {transform = {position = Vector3.right, rotation = Quaternion.identity}}; var second = new GameObject {transform = {position = Vector3.right, rotation = Quaternion.identity}};
@ -103,7 +109,8 @@ namespace X10D.Unity.Tests
second.SetParent(first); second.SetParent(first);
Assert.That(second.transform.parent, Is.EqualTo(first.transform)); Assert.That(second.transform.parent, Is.EqualTo(first.transform));
yield break; Object.Destroy(first);
Object.Destroy(second);
} }
} }
} }

View File

@ -70,4 +70,19 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Update="src\ExceptionMessages.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>ExceptionMessages.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="src\ExceptionMessages.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>ExceptionMessages.resx</DependentUpon>
</Compile>
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,80 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace X10D.Unity {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class ExceptionMessages {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal ExceptionMessages() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("X10D.Unity.src.ExceptionMessages", typeof(ExceptionMessages).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to The game object {0} already has a component of type {1}..
/// </summary>
internal static string ComponentAlreadyExists {
get {
return ResourceManager.GetString("ComponentAlreadyExists", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The game object {0} does not have a component of type {1}..
/// </summary>
internal static string ComponentDoesNotExist {
get {
return ResourceManager.GetString("ComponentDoesNotExist", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="ComponentDoesNotExist" xml:space="preserve">
<value>The game object {0} does not have a component of type {1}.</value>
</data>
<data name="ComponentAlreadyExists" xml:space="preserve">
<value>The game object {0} already has a component of type {1}.</value>
</data>
</root>

View File

@ -15,6 +15,7 @@ public static class GameObjectExtensions
/// <returns>An array <typeparamref name="T" /> representing the child components.</returns> /// <returns>An array <typeparamref name="T" /> representing the child components.</returns>
public static T[] GetComponentsInChildrenOnly<T>(this GameObject gameObject) public static T[] GetComponentsInChildrenOnly<T>(this GameObject gameObject)
{ {
Transform rootTransform = gameObject.transform;
var components = new List<T>(gameObject.GetComponentsInChildren<T>()); var components = new List<T>(gameObject.GetComponentsInChildren<T>());
for (var index = 0; index < components.Count; index++) for (var index = 0; index < components.Count; index++)
@ -26,7 +27,7 @@ public static class GameObjectExtensions
continue; continue;
} }
if (childComponent.transform.parent != gameObject.transform) if (childComponent.transform == rootTransform)
{ {
components.RemoveAt(index); components.RemoveAt(index);
index--; index--;

View File

@ -1,9 +1,4 @@
#if NET5_0_OR_GREATER namespace X10D.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
#endif
namespace X10D.Collections;
/// <summary> /// <summary>
/// Extension methods for <see cref="Span{T}" /> and <see cref="ReadOnlySpan{T}" /> /// Extension methods for <see cref="Span{T}" /> and <see cref="ReadOnlySpan{T}" />

View File

@ -0,0 +1,48 @@
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)
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(observers);
ArgumentNullException.ThrowIfNull(observer);
#else
if (observers is null)
{
throw new ArgumentNullException(nameof(observers));
}
if (observer is null)
{
throw new ArgumentNullException(nameof(observer));
}
#endif
_observers = observers;
_observer = 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,97 @@
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 NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(progress);
#else
if (progress is null)
{
throw new ArgumentNullException(nameof(progress));
}
#endif
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 NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(progress);
#else
if (progress is null)
{
throw new ArgumentNullException(nameof(progress));
}
#endif
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,40 @@
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 NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(observer);
#else
if (observer is null)
{
throw new ArgumentNullException(nameof(observer));
}
#endif
_observers.Add(observer);
return new ObservableDisposer<T>(_observers, observer, OnDispose);
}
}