diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de5308..6cdf854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 4.0.0 - [Unreleased] ### Added + - X10D: Added extension methods for `DateOnly`, for parity with `DateTime` and `DateTimeOffset`. - X10D: Added math-related extension methods for `BigInteger`. - X10D: Added `Span.Replace(T, T)`. - X10D: Added `CountDigits` for integer types. +- X10D: Added `Progress.OnProgressChanged([T])`; - X10D: Added `TextWriter.WriteNoAlloc(int[, ReadOnlySpan[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteNoAlloc(uint[, ReadOnlySpan[, IFormatProvider]])`. - X10D: Added `TextWriter.WriteNoAlloc(long[, ReadOnlySpan[, 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[, IFormatProvider]])`. ### Changed + - X10D: `DateTime.Age(DateTime)` and `DateTimeOffset.Age(DateTimeOffset)` parameter renamed from `asOf` to `referenceDate`. ## [3.2.0] - 2023-04-03 diff --git a/X10D.Tests/X10D.Tests.csproj b/X10D.Tests/X10D.Tests.csproj index d763c8c..00ea605 100644 --- a/X10D.Tests/X10D.Tests.csproj +++ b/X10D.Tests/X10D.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/X10D.Tests/src/Reactive/ProgressTests.cs b/X10D.Tests/src/Reactive/ProgressTests.cs new file mode 100644 index 0000000..fce2683 --- /dev/null +++ b/X10D.Tests/src/Reactive/ProgressTests.cs @@ -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(); + progress.OnProgressChanged(1.0f).Subscribe(_ => subscriberWasCalled = true, () => completionWasCalled = true); + + ((IProgress)progress).Report(0.5f); + ((IProgress)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(); + progress.OnProgressChanged().Subscribe(_ => subscriberWasCalled = true); + + ((IProgress)progress).Report(0.5f); + + Thread.Sleep(1000); + Assert.That(subscriberWasCalled); + } + + [Test] + public void OnProgressChanged_ShouldCallSubscribers_OnProgressChanged_GivenCompletionValue() + { + var subscriberWasCalled = false; + + var progress = new Progress(); + progress.OnProgressChanged(1.0f).Subscribe(_ => subscriberWasCalled = true); + + ((IProgress)progress).Report(0.5f); + + Thread.Sleep(1000); + Assert.That(subscriberWasCalled); + } + + [Test] + public void OnProgressChanged_ShouldThrowArgumentNullException_GivenNullProgress() + { + Progress progress = null!; + Assert.Throws(() => progress.OnProgressChanged()); + } + + [Test] + public void OnProgressChanged_ShouldThrowArgumentNullException_GivenNullProgressAndCompletionValue() + { + Progress progress = null!; + Assert.Throws(() => progress.OnProgressChanged(1.0f)); + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs index e6c1431..126e435 100644 --- a/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/ComponentTests.cs @@ -4,13 +4,14 @@ using System.Collections; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; +using Object = UnityEngine.Object; namespace X10D.Unity.Tests { public class ComponentTests { - [UnityTest] - public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent() + [Test] + public void GetComponentsInChildrenOnly_ShouldIgnoreParent() { var parent = new GameObject(); var rigidbody = parent.AddComponent(); @@ -20,10 +21,11 @@ namespace X10D.Unity.Tests child.AddComponent(); Rigidbody[] components = rigidbody.GetComponentsInChildrenOnly(); - Assert.That(components.Length, Is.EqualTo(1)); + Assert.That(components, Has.Length.EqualTo(1)); Assert.That(child, Is.EqualTo(components[0].gameObject)); - yield break; + Object.Destroy(parent); + Object.Destroy(child); } } } diff --git a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs index fef5a66..565d455 100644 --- a/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/GameObjectTests.cs @@ -1,16 +1,16 @@ #nullable enable -using System.Collections; +using System.Diagnostics.CodeAnalysis; using NUnit.Framework; using UnityEngine; -using UnityEngine.TestTools; +using Object = UnityEngine.Object; namespace X10D.Unity.Tests { public class GameObjectTests { - [UnityTest] - public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent() + [Test] + public void GetComponentsInChildrenOnly_ShouldIgnoreParent() { var parent = new GameObject(); parent.AddComponent(); @@ -20,14 +20,16 @@ namespace X10D.Unity.Tests child.AddComponent(); Rigidbody[] components = parent.GetComponentsInChildrenOnly(); - Assert.That(components.Length, Is.EqualTo(1)); + Assert.That(components, Has.Length.EqualTo(1)); Assert.That(child, Is.EqualTo(components[0].gameObject)); - yield break; + Object.Destroy(parent); + Object.Destroy(child); } - [UnityTest] - public IEnumerator LookAt_ShouldRotateSameAsTransform() + [Test] + [SuppressMessage("ReSharper", "Unity.InefficientPropertyAccess", Justification = "False positive.")] + public void LookAt_ShouldRotateSameAsTransform() { var first = new GameObject {transform = {position = Vector3.zero, 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); Assert.That(firstTransform.rotation, Is.EqualTo(expected)); - yield break; + Object.Destroy(first); + Object.Destroy(second); } - [UnityTest] - public IEnumerator SetLayerRecursively_ShouldSetLayerRecursively() + [Test] + public void SetLayerRecursively_ShouldSetLayerRecursively() { var parent = new GameObject(); var child = new GameObject(); @@ -82,11 +85,14 @@ namespace X10D.Unity.Tests Assert.That(child.layer, Is.EqualTo(layer)); Assert.That(grandChild.layer, Is.EqualTo(layer)); - yield break; + Object.Destroy(parent); + Object.Destroy(child); + Object.Destroy(grandChild); } - [UnityTest] - public IEnumerator SetParent_ShouldSetParent() + [Test] + [SuppressMessage("ReSharper", "Unity.InefficientPropertyAccess", Justification = "False positive.")] + public void SetParent_ShouldSetParent() { var first = new GameObject {transform = {position = Vector3.zero, 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); Assert.That(second.transform.parent, Is.EqualTo(first.transform)); - yield break; + Object.Destroy(first); + Object.Destroy(second); } } } diff --git a/X10D.Unity/X10D.Unity.csproj b/X10D.Unity/X10D.Unity.csproj index f99bf2e..90caf27 100644 --- a/X10D.Unity/X10D.Unity.csproj +++ b/X10D.Unity/X10D.Unity.csproj @@ -70,4 +70,19 @@ + + + ResXFileCodeGenerator + ExceptionMessages.Designer.cs + + + + + + True + True + ExceptionMessages.resx + + + diff --git a/X10D.Unity/src/ExceptionMessages.Designer.cs b/X10D.Unity/src/ExceptionMessages.Designer.cs new file mode 100644 index 0000000..954a74a --- /dev/null +++ b/X10D.Unity/src/ExceptionMessages.Designer.cs @@ -0,0 +1,80 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace X10D.Unity { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // 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() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [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; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The game object {0} already has a component of type {1}.. + /// + internal static string ComponentAlreadyExists { + get { + return ResourceManager.GetString("ComponentAlreadyExists", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The game object {0} does not have a component of type {1}.. + /// + internal static string ComponentDoesNotExist { + get { + return ResourceManager.GetString("ComponentDoesNotExist", resourceCulture); + } + } + } +} diff --git a/X10D.Unity/src/ExceptionMessages.resx b/X10D.Unity/src/ExceptionMessages.resx new file mode 100644 index 0000000..ec27743 --- /dev/null +++ b/X10D.Unity/src/ExceptionMessages.resx @@ -0,0 +1,34 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + + The game object {0} does not have a component of type {1}. + + + + The game object {0} already has a component of type {1}. + + \ No newline at end of file diff --git a/X10D.Unity/src/GameObjectExtensions.cs b/X10D.Unity/src/GameObjectExtensions.cs index 97d03d3..23d7f3c 100644 --- a/X10D.Unity/src/GameObjectExtensions.cs +++ b/X10D.Unity/src/GameObjectExtensions.cs @@ -15,6 +15,7 @@ public static class GameObjectExtensions /// An array representing the child components. public static T[] GetComponentsInChildrenOnly(this GameObject gameObject) { + Transform rootTransform = gameObject.transform; var components = new List(gameObject.GetComponentsInChildren()); for (var index = 0; index < components.Count; index++) @@ -26,7 +27,7 @@ public static class GameObjectExtensions continue; } - if (childComponent.transform.parent != gameObject.transform) + if (childComponent.transform == rootTransform) { components.RemoveAt(index); index--; diff --git a/X10D/src/Collections/SpanExtensions.cs b/X10D/src/Collections/SpanExtensions.cs index 1aee61d..ac2fd2b 100644 --- a/X10D/src/Collections/SpanExtensions.cs +++ b/X10D/src/Collections/SpanExtensions.cs @@ -1,9 +1,4 @@ -#if NET5_0_OR_GREATER -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -#endif - -namespace X10D.Collections; +namespace X10D.Collections; /// /// Extension methods for and diff --git a/X10D/src/Reactive/ObservableDisposer.cs b/X10D/src/Reactive/ObservableDisposer.cs new file mode 100644 index 0000000..9073ff9 --- /dev/null +++ b/X10D/src/Reactive/ObservableDisposer.cs @@ -0,0 +1,48 @@ +namespace X10D.Reactive; + +/// +/// Represents a disposable that removes an observer from a collection of observers. +/// +internal readonly struct ObservableDisposer : IDisposable +{ + private readonly HashSet> _observers; + private readonly IObserver _observer; + private readonly Action? _additionalAction; + + /// + /// Initializes a new instance of the struct. + /// + /// A collection of observers from which to remove the specified observer. + /// The observer to remove from the collection. + /// The additional action to run on dispose. + public ObservableDisposer(HashSet> observers, IObserver 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; + } + + /// + /// Removes the observer from the collection of observers. + /// + public void Dispose() + { + _observers.Remove(_observer); + _additionalAction?.Invoke(); + } +} diff --git a/X10D/src/Reactive/ProgressExtensions.cs b/X10D/src/Reactive/ProgressExtensions.cs new file mode 100644 index 0000000..6128a72 --- /dev/null +++ b/X10D/src/Reactive/ProgressExtensions.cs @@ -0,0 +1,97 @@ +namespace X10D.Reactive; + +/// +/// Provides extension methods for . +/// +public static class ProgressExtensions +{ + /// + /// Wraps the event of the current in an + /// object. + /// + /// The progress whose event to wrap. + /// The type of progress update value. + /// + /// An object that wraps the event of the current + /// . + /// + /// is . + public static IObservable OnProgressChanged(this Progress progress) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(progress); +#else + if (progress is null) + { + throw new ArgumentNullException(nameof(progress)); + } +#endif + + var progressObservable = new ProgressObservable(); + + void ProgressChangedHandler(object? sender, T args) + { + IObserver[] 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; + } + + /// + /// Wraps the event of the current in an + /// object, and completes the observable when the progress reaches the specified value. + /// + /// The progress whose event to wrap. + /// The value that indicates completion. + /// The type of progress update value. + /// + /// An object that wraps the event of the current + /// . + /// + /// is . + public static IObservable OnProgressChanged(this Progress 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(); + var comparer = EqualityComparer.Default; + + void ProgressChangedHandler(object? sender, T args) + { + IObserver[] 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; + } +} diff --git a/X10D/src/Reactive/ProgressObservable.cs b/X10D/src/Reactive/ProgressObservable.cs new file mode 100644 index 0000000..29a8243 --- /dev/null +++ b/X10D/src/Reactive/ProgressObservable.cs @@ -0,0 +1,40 @@ +namespace X10D.Reactive; + +/// +/// Represents a concrete implementation of that tracks progress of a . +/// +internal sealed class ProgressObservable : IObservable +{ + private readonly HashSet> _observers = new(); + + /// + /// Gets the observers. + /// + /// The observers. + public IObserver[] Observers + { + get => _observers.ToArray(); + } + + internal Action? OnDispose { get; set; } + + /// + /// Subscribes the specified observer to the progress tracker. + /// + /// The observer. + /// An object which can be disposed to unsubscribe from progress tracking. + public IDisposable Subscribe(IObserver 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(_observers, observer, OnDispose); + } +}