From f2d157fd2b8ea04e75038ecc9021131a666bda2b Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sat, 8 Mar 2025 06:05:28 +0100 Subject: [PATCH 1/5] feat: add IsApproximately for all types implementing INumber Signed-off-by: Kenny Pflug --- .../CommonAssertions/IsApproximatelyTests.cs | 58 ++++++++++++++++++- .../Check.IsApproximately.cs | 24 +++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs index c0abf2d..afbf5af 100644 --- a/Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs +++ b/Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs @@ -38,4 +38,60 @@ public static void FloatWithDefaultTolerance(float first, float second, bool exp [InlineData(5.0f, 15.0001f, 10.0f, false)] public static void FloatWithCustomTolerance(float first, float second, float tolerance, bool expected) => first.IsApproximately(second, tolerance).Should().Be(expected); -} \ No newline at end of file + +#if NET8_0 + [Theory] + [InlineData(1.1, 1.3, 0.5, true)] + [InlineData(100.55, 100.555, 0.00001, false)] + [InlineData(5.0, 14.999999, 10.0, true)] + [InlineData(5.0, 15.0, 10.0, false)] + [InlineData(5.0, 15.0001, 10.0, false)] + public static void GenericDoubleWithCustomTolerance(double first, double second, double tolerance, bool expected) => + first.IsApproximately(second, tolerance).Should().Be(expected); + + [Theory] + [InlineData(1.1f, 1.3f, 0.5f, true)] + [InlineData(100.55f, 100.555f, 0.00001f, false)] + [InlineData(5.0f, 14.999999f, 10.0f, true)] + [InlineData(5.0f, 15.0f, 10.0f, false)] + [InlineData(5.0f, 15.0001f, 10.0f, false)] + public static void GenericFloatWithCustomTolerance(float first, float second, float tolerance, bool expected) => + first.IsApproximately(second, tolerance).Should().Be(expected); + + [Theory] + [InlineData(5, 10, 10, true)] + [InlineData(5, 15, 10, false)] + [InlineData(-5, 5, 12, true)] + [InlineData(-100, 100, 199, false)] + [InlineData(42, 42, 1, true)] + public static void GenericIntWithCustomTolerance(int first, int second, int tolerance, bool expected) => + first.IsApproximately(second, tolerance).Should().Be(expected); + + [Theory] + [InlineData(5L, 10L, 10L, true)] + [InlineData(5L, 15L, 10L, false)] + [InlineData(-5L, 5L, 12L, true)] + [InlineData(-100L, 100L, 199L, false)] + [InlineData(42L, 42L, 1L, true)] + public static void GenericLongWithCustomTolerance(long first, long second, long tolerance, bool expected) => + first.IsApproximately(second, tolerance).Should().Be(expected); + + [Theory] + [MemberData(nameof(DecimalTestData))] + public static void GenericDecimalWithCustomTolerance( + decimal first, + decimal second, + decimal tolerance, + bool expected + ) => + first.IsApproximately(second, tolerance).Should().Be(expected); + + public static TheoryData DecimalTestData() => new () + { + { 1.1m, 1.3m, 0.5m, true }, + { 100.55m, 100.555m, 0.00001m, false }, + { 5.0m, 14.999999m, 10.0m, true }, + { 5.0m, 15.0m, 10.0m, false }, + }; +#endif +} diff --git a/Code/Light.GuardClauses/Check.IsApproximately.cs b/Code/Light.GuardClauses/Check.IsApproximately.cs index 698450e..6ee6172 100644 --- a/Code/Light.GuardClauses/Check.IsApproximately.cs +++ b/Code/Light.GuardClauses/Check.IsApproximately.cs @@ -1,5 +1,8 @@ using System; using System.Runtime.CompilerServices; +#if NET8_0 +using System.Numerics; +#endif namespace Light.GuardClauses; @@ -35,8 +38,8 @@ public static bool IsApproximately(this double value, double other) => /// /// Checks if the specified value is approximately the same as the other value, using the given tolerance. /// - /// The first value to compare. - /// The second value to compare. + /// The first value to be compared. + /// The second value to be compared. /// The tolerance indicating how much the two values may differ from each other. /// /// True if and are equal or if their absolute difference @@ -58,4 +61,21 @@ public static bool IsApproximately(this float value, float other, float toleranc [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsApproximately(this float value, float other) => Math.Abs(value - other) < 0.0001f; + +#if NET8_0 + /// + /// Checks if the specified value is approximately the same as the other value, using the given tolerance. + /// + /// The first value to be compared. + /// The second value to be compared. + /// The tolerance indicating how much the two values may differ from each other. + /// The type that implements the interface. + /// + /// True if and are equal or if their absolute difference + /// is smaller than the given , otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsApproximately(this T value, T other, T tolerance) where T : INumber => + T.Abs(value - other) < tolerance; +#endif } From 7aeddc84173ba1aca5159c71bfddd12418f69fb8 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sat, 8 Mar 2025 07:41:51 +0100 Subject: [PATCH 2/5] feat: add MustBeApproximately Signed-off-by: Kenny Pflug --- .../MustBeApproximatelyTests.cs | 236 ++++++++++++++ .../Check.MustBeApproximately.cs | 297 ++++++++++++++++++ .../Throw.MustBeApproximately.cs | 28 ++ 3 files changed, 561 insertions(+) create mode 100644 Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeApproximatelyTests.cs create mode 100644 Code/Light.GuardClauses/Check.MustBeApproximately.cs create mode 100644 Code/Light.GuardClauses/ExceptionFactory/Throw.MustBeApproximately.cs diff --git a/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeApproximatelyTests.cs new file mode 100644 index 0000000..3204655 --- /dev/null +++ b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeApproximatelyTests.cs @@ -0,0 +1,236 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Light.GuardClauses.Tests.ComparableAssertions; + +public static class MustBeApproximatelyTests +{ + [Theory] + [InlineData(5.1, 5.0, 0.2)] + [InlineData(10.3, 10.3, 0.01)] + [InlineData(3.14159, 3.14, 0.002)] + [InlineData(-42.0, -42.0001, 0.001)] + public static void ValuesApproximatelyEqual_Double(double value, double other, double tolerance) => + value.MustBeApproximately(other, tolerance).Should().Be(value); + + [Theory] + [InlineData(5.1f, 5.0f, 0.2f)] + [InlineData(10.3f, 10.3f, 0.01f)] + [InlineData(3.14159f, 3.14f, 0.002f)] + [InlineData(-42.0f, -42.0001f, 0.001f)] + public static void ValuesApproximatelyEqual_Float(float value, float other, float tolerance) => + value.MustBeApproximately(other, tolerance).Should().Be(value); + + [Theory] + [InlineData(5.0, 5.3, 0.1)] + [InlineData(100.0, 99.8, 0.1)] + [InlineData(-20.0, -20.2, 0.1)] + [InlineData(0.0001, 0.0002, 0.00005)] + public static void ValuesNotApproximatelyEqual_Double(double value, double other, double tolerance) + { + var act = () => value.MustBeApproximately(other, tolerance, nameof(value)); + + var exceptionAssertion = act.Should().Throw().Which; + exceptionAssertion.Message.Should().Contain( + $"{nameof(value)} must be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}." + ); + exceptionAssertion.ParamName.Should().BeSameAs(nameof(value)); + } + + [Theory] + [InlineData(5.0f, 5.3f, 0.1f)] + [InlineData(100.0f, 99.8f, 0.1f)] + [InlineData(-20.0f, -20.2f, 0.1f)] + [InlineData(0.0001f, 0.0002f, 0.00005f)] + public static void ValuesNotApproximatelyEqual_Float(float value, float other, float tolerance) + { + var act = () => value.MustBeApproximately(other, tolerance, nameof(value)); + + var exceptionAssertion = act.Should().Throw().Which; + exceptionAssertion.Message.Should().Contain( + $"{nameof(value)} must be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}." + ); + exceptionAssertion.ParamName.Should().BeSameAs(nameof(value)); + } + + [Fact] + public static void DefaultTolerance_Double() + { + // Should pass - difference is 0.00005 which is less than default tolerance 0.0001 + const double value = 1.00005; + value.MustBeApproximately(1.0).Should().Be(value); + + // Should throw - difference is 0.0002 which is greater than default tolerance 0.0001 + Action act = () => 1.0002.MustBeApproximately(1.0, "parameter"); + act.Should().Throw() + .WithParameterName("parameter"); + } + + [Fact] + public static void DefaultTolerance_Float() + { + // Should pass - difference is 0.00005f which is less than default tolerance 0.0001f + const float value = 1.00005f; + value.MustBeApproximately(1.0f).Should().Be(value); + + // Should throw - difference is 0.0002f which is greater than default tolerance 0.0001f + Action act = () => 1.0002f.MustBeApproximately(1.0f, "parameter"); + act.Should().Throw() + .WithParameterName("parameter"); + } + + [Fact] + public static void CustomException_Double() => + Test.CustomException( + 5.0, + 5.3, + (x, y, exceptionFactory) => x.MustBeApproximately(y, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionWithTolerance_Double() => + Test.CustomException( + 5.0, + 5.5, + 0.1, + (x, y, z, exceptionFactory) => x.MustBeApproximately(y, z, exceptionFactory) + ); + + [Fact] + public static void CustomException_Float() => + Test.CustomException( + 5.0f, + 5.3f, + (x, y, exceptionFactory) => x.MustBeApproximately(y, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionWithTolerance_Float() => + Test.CustomException( + 5.0f, + 5.5f, + 0.1f, + (x, y, z, exceptionFactory) => x.MustBeApproximately(y, z, exceptionFactory) + ); + + [Fact] + public static void NoCustomExceptionThrown_Double() => + 5.0.MustBeApproximately(5.05, 0.1, (_, _, _) => null).Should().Be(5.0); + + [Fact] + public static void NoCustomExceptionThrown_Float() => + 5.0f.MustBeApproximately(5.05f, 0.1f, (_, _, _) => null).Should().Be(5.0f); + + [Fact] + public static void CustomMessage_Double() => + Test.CustomMessage( + message => 100.0.MustBeApproximately(101.0, 0.5, message: message) + ); + + [Fact] + public static void CustomMessage_Float() => + Test.CustomMessage( + message => 100.0f.MustBeApproximately(101.0f, 0.5f, message: message) + ); + + [Fact] + public static void CallerArgumentExpression_Double() + { + const double seventyEightO1 = 78.1; + + var act = () => seventyEightO1.MustBeApproximately(3.0); + + act.Should().Throw() + .WithParameterName(nameof(seventyEightO1)); + } + + [Fact] + public static void CallerArgumentExpressionWithTolerance_Double() + { + const double pi = 3.14159; + + var act = () => pi.MustBeApproximately(3.0, 0.1); + + act.Should().Throw() + .WithParameterName(nameof(pi)); + } + + [Fact] + public static void CallerArgumentExpression_Float() + { + const float seventyEightO1 = 78.1f; + + var act = () => seventyEightO1.MustBeApproximately(3.0f); + + act.Should().Throw() + .WithParameterName(nameof(seventyEightO1)); + } + + [Fact] + public static void CallerArgumentExpressionWithTolerance_Float() + { + const float pi = 3.14159f; + + var act = () => pi.MustBeApproximately(3.0f, 0.1f); + + act.Should().Throw() + .WithParameterName(nameof(pi)); + } + +#if NET8_0 + [Theory] + [InlineData(5.1, 5.0, 0.2)] + [InlineData(10.3, 10.3, 0.01)] + [InlineData(3.14159, 3.14, 0.002)] + [InlineData(-42.0, -42.0001, 0.001)] + public static void ValuesApproximatelyEqual_Generic(double value, double other, double tolerance) => + value.MustBeApproximately(other, tolerance).Should().Be(value); + + [Theory] + [InlineData(5.0, 5.3, 0.1)] + [InlineData(100.0, 99.8, 0.1)] + [InlineData(-20.0, -20.2, 0.1)] + [InlineData(0.0001, 0.0002, 0.00005)] + public static void ValuesNotApproximatelyEqual_Generic(double value, double other, double tolerance) + { + var act = () => value.MustBeApproximately(other, tolerance, nameof(value)); + + var exceptionAssertion = act.Should().Throw().Which; + exceptionAssertion.Message.Should().Contain( + $"{nameof(value)} must be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}." + ); + exceptionAssertion.ParamName.Should().BeSameAs(nameof(value)); + } + + [Fact] + public static void CustomExceptionWithTolerance_Generic() => + Test.CustomException( + 5.0, + 5.3, + 0.2, + (x, y, t, exceptionFactory) => x.MustBeApproximately(y, t, exceptionFactory) + ); + + [Fact] + public static void NoCustomExceptionThrown_Generic() => + 5.0.MustBeApproximately(5.05, 0.1, (_, _, _) => null).Should().Be(5.0); + + [Fact] + public static void CustomMessage_Generic() => + Test.CustomMessage( + message => 100.0.MustBeApproximately(101.0, 0.5, message: message) + ); + + [Fact] + public static void CallerArgumentExpressionWithTolerance_Generic() + { + const double seventyEightO1 = 78.1; + + var act = () => seventyEightO1.MustBeApproximately(3.0, 10.0); + + act.Should().Throw() + .WithParameterName(nameof(seventyEightO1)); + } +#endif +} diff --git a/Code/Light.GuardClauses/Check.MustBeApproximately.cs b/Code/Light.GuardClauses/Check.MustBeApproximately.cs new file mode 100644 index 0000000..77bdf05 --- /dev/null +++ b/Code/Light.GuardClauses/Check.MustBeApproximately.cs @@ -0,0 +1,297 @@ +using System; +using System.Runtime.CompilerServices; +using Light.GuardClauses.ExceptionFactory; +#if NET8_0 +using System.Numerics; +#endif + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the specified is approximately equal to the given + /// value, using the default tolerance of 0.0001, or otherwise throws an + /// . + /// + /// The value to be checked. + /// The value that should be approximately equal to. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when the absolute difference between and is not + /// less than 0.0001. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeApproximately( + this double parameter, + double other, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) => + parameter.MustBeApproximately(other, 0.0001, parameterName, message); + + /// + /// Ensures that the specified is approximately equal to the given + /// value, using the default tolerance of 0.0001, or otherwise throws an + /// . + /// + /// The value to be checked. + /// The value that should be approximately equal to. + /// + /// The delegate that creates your custom exception. and + /// are passed to this delegate. + /// + /// + /// Thrown when the absolute difference between and is not + /// less than 0.0001. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeApproximately( + this double parameter, + double other, + Func exceptionFactory + ) + { + if (!parameter.IsApproximately(other)) + { + Throw.CustomException(exceptionFactory, parameter, other); + } + + return parameter; + } + + /// + /// Ensures that the specified is approximately equal to the given + /// value, or otherwise throws an . + /// + /// The value to be checked. + /// The value that should be approximately equal to. + /// The tolerance indicating how much the two values may differ from each other. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when the absolute difference between and is not + /// less than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeApproximately( + this double parameter, + double other, + double tolerance, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsApproximately(other, tolerance)) + { + Throw.MustBeApproximately(parameter, other, tolerance, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is approximately equal to the given + /// value, or otherwise throws your custom exception. + /// + /// The value to be checked. + /// The value that should be approximately equal to. + /// The tolerance indicating how much the two values may differ from each other. + /// The delegate that creates your custom exception. , + /// , and are passed to this delegate. + /// + /// Your custom exception thrown when the absolute difference between and + /// is not less than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeApproximately( + this double parameter, + double other, + double tolerance, + Func exceptionFactory + ) + { + if (!parameter.IsApproximately(other, tolerance)) + { + Throw.CustomException(exceptionFactory, parameter, other, tolerance); + } + + return parameter; + } + + /// + /// Ensures that the specified is approximately equal to the given + /// value, using the default tolerance of 0.0001f, or otherwise throws an + /// . + /// + /// The value to be checked. + /// The value that should be approximately equal to. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when the absolute difference between and is + /// not less than 0.0001f. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeApproximately( + this float parameter, + float other, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + parameter.MustBeApproximately(other, 0.0001f, parameterName, message); + + /// + /// Ensures that the specified is approximately equal to the given + /// value, using the default tolerance of 0.0001, or otherwise throws an + /// . + /// + /// The value to be checked. + /// The value that should be approximately equal to. + /// + /// The delegate that creates your custom exception. and + /// are passed to this delegate. + /// + /// + /// Thrown when the absolute difference between and is not + /// less than 0.0001. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeApproximately( + this float parameter, + float other, + Func exceptionFactory + ) + { + if (!parameter.IsApproximately(other)) + { + Throw.CustomException(exceptionFactory, parameter, other); + } + + return parameter; + } + + /// + /// Ensures that the specified is approximately equal to the given + /// value, or otherwise throws an . + /// + /// The value to be checked. + /// The value that should be approximately equal to. + /// The tolerance indicating how much the two values may differ from each other. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when the absolute difference between and is not + /// less than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeApproximately( + this float parameter, + float other, + float tolerance, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsApproximately(other, tolerance)) + { + Throw.MustBeApproximately(parameter, other, tolerance, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is approximately equal to the given + /// value, or otherwise throws your custom exception. + /// + /// The value to be checked. + /// The value that should be approximately equal to. + /// The tolerance indicating how much the two values may differ from each other. + /// + /// The delegate that creates your custom exception. , , and + /// are passed to this delegate. + /// + /// + /// Your custom exception thrown when the absolute difference between and + /// is not less than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeApproximately( + this float parameter, + float other, + float tolerance, + Func exceptionFactory + ) + { + if (!parameter.IsApproximately(other, tolerance)) + { + Throw.CustomException(exceptionFactory, parameter, other, tolerance); + } + + return parameter; + } + +#if NET8_0 + /// + /// Ensures that the specified is approximately equal to the given + /// value, or otherwise throws an . + /// + /// The value to be checked. + /// The value that should be approximately equal to. + /// The tolerance indicating how much the two values may differ from each other. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The type that implements the interface. + /// + /// Thrown when the absolute difference between and is not + /// less than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T MustBeApproximately( + this T parameter, + T other, + T tolerance, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) where T : INumber + { + if (!parameter.IsApproximately(other, tolerance)) + { + Throw.MustBeApproximately(parameter, other, tolerance, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is approximately equal to the given + /// value, or otherwise throws your custom exception. + /// + /// The value to be checked. + /// The value that should be approximately equal to. + /// The tolerance indicating how much the two values may differ from each other. + /// The delegate that creates your custom exception. , + /// , and are passed to this delegate. + /// The type that implements the interface. + /// + /// Your custom exception thrown when the absolute difference between and + /// is not less than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T MustBeApproximately( + this T parameter, + T other, + T tolerance, + Func exceptionFactory + ) where T : INumber + { + if (!parameter.IsApproximately(other, tolerance)) + { + Throw.CustomException(exceptionFactory, parameter, other, tolerance); + } + + return parameter; + } +#endif +} diff --git a/Code/Light.GuardClauses/ExceptionFactory/Throw.MustBeApproximately.cs b/Code/Light.GuardClauses/ExceptionFactory/Throw.MustBeApproximately.cs new file mode 100644 index 0000000..e4ba742 --- /dev/null +++ b/Code/Light.GuardClauses/ExceptionFactory/Throw.MustBeApproximately.cs @@ -0,0 +1,28 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace Light.GuardClauses.ExceptionFactory; + +public static partial class Throw +{ + /// + /// Throws the default indicating that a value must be approximately + /// equal to another value within a specified tolerance, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void MustBeApproximately( + T parameter, + T other, + T tolerance, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + throw new ArgumentOutOfRangeException( + parameterName, + message ?? + $"{parameterName ?? "The value"} must be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {parameter}." + ); +} From 4e99de6997b794c654e534b794676158786df048 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sat, 8 Mar 2025 08:01:44 +0100 Subject: [PATCH 3/5] build: upgrade Light.GuardClauses.Performance to .NET 8 Signed-off-by: Kenny Pflug --- .../Light.GuardClauses.Performance.csproj | 2 +- .../Light.GuardClauses.Performance/Program.cs | 23 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Code/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj b/Code/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj index 7ffe52c..cbf1418 100644 --- a/Code/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj +++ b/Code/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/Code/Light.GuardClauses.Performance/Program.cs b/Code/Light.GuardClauses.Performance/Program.cs index 6bdd825..b17928a 100644 --- a/Code/Light.GuardClauses.Performance/Program.cs +++ b/Code/Light.GuardClauses.Performance/Program.cs @@ -4,18 +4,17 @@ using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; -namespace Light.GuardClauses.Performance +namespace Light.GuardClauses.Performance; + +public static class Program { - public static class Program - { - private static IConfig DefaultConfiguration => - DefaultConfig - .Instance - .AddJob(Job.Default.WithRuntime(CoreRuntime.Core70)) - .AddJob(Job.Default.WithRuntime(ClrRuntime.Net48)) - .AddDiagnoser(MemoryDiagnoser.Default, new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig())); + private static IConfig DefaultConfiguration => + DefaultConfig + .Instance + .AddJob(Job.Default.WithRuntime(CoreRuntime.Core80)) + .AddJob(Job.Default.WithRuntime(ClrRuntime.Net48)) + .AddDiagnoser(MemoryDiagnoser.Default, new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig())); - public static void Main(string[] arguments) => - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(arguments, DefaultConfiguration); - } + public static void Main(string[] arguments) => + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(arguments, DefaultConfiguration); } \ No newline at end of file From b13929909d311c7c0a17a94a0f6dc8afba525793 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sat, 8 Mar 2025 08:02:02 +0100 Subject: [PATCH 4/5] performance: add MustBeApproximatelyBenchmark Signed-off-by: Kenny Pflug --- .../MustBeApproximatelyBenchmark.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Code/Light.GuardClauses.Performance/ComparableAssertions/MustBeApproximatelyBenchmark.cs diff --git a/Code/Light.GuardClauses.Performance/ComparableAssertions/MustBeApproximatelyBenchmark.cs b/Code/Light.GuardClauses.Performance/ComparableAssertions/MustBeApproximatelyBenchmark.cs new file mode 100644 index 0000000..57364eb --- /dev/null +++ b/Code/Light.GuardClauses.Performance/ComparableAssertions/MustBeApproximatelyBenchmark.cs @@ -0,0 +1,25 @@ +using System; +using BenchmarkDotNet.Attributes; + +namespace Light.GuardClauses.Performance.ComparableAssertions; + +[MemoryDiagnoser] +// ReSharper disable once ClassCanBeSealed.Global -- Benchmark.NET derives from this class with dynamically created code +public class MustBeApproximatelyBenchmark +{ + [Benchmark] + public double MustBeApproximately() => 5.100001.MustBeApproximately(5.100000, 0.0001); + + [Benchmark(Baseline = true)] + public double Imperative() => Imperative(5.100001, 5.100000, 0.0001); + + private static double Imperative(double parameter, double other, double tolerance) + { + if (Math.Abs(parameter - other) > tolerance) + { + throw new ArgumentOutOfRangeException(nameof(parameter), $"Value is not approximately {other}"); + } + + return parameter; + } +} From 66be15f9f94a73813b7f05256faea3655632482c Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sat, 8 Mar 2025 09:39:51 +0100 Subject: [PATCH 5/5] feat: add MustNotBeApproximately Signed-off-by: Kenny Pflug --- .../MustNotBeApproximatelyBenchmark.cs | 25 ++ .../MustNotBeApproximatelyTests.cs | 238 ++++++++++++++ .../Check.MustNotBeApproximately.cs | 297 ++++++++++++++++++ .../Throw.MustNotBeApproximately.cs | 28 ++ 4 files changed, 588 insertions(+) create mode 100644 Code/Light.GuardClauses.Performance/ComparableAssertions/MustNotBeApproximatelyBenchmark.cs create mode 100644 Code/Light.GuardClauses.Tests/ComparableAssertions/MustNotBeApproximatelyTests.cs create mode 100644 Code/Light.GuardClauses/Check.MustNotBeApproximately.cs create mode 100644 Code/Light.GuardClauses/ExceptionFactory/Throw.MustNotBeApproximately.cs diff --git a/Code/Light.GuardClauses.Performance/ComparableAssertions/MustNotBeApproximatelyBenchmark.cs b/Code/Light.GuardClauses.Performance/ComparableAssertions/MustNotBeApproximatelyBenchmark.cs new file mode 100644 index 0000000..a65d4b8 --- /dev/null +++ b/Code/Light.GuardClauses.Performance/ComparableAssertions/MustNotBeApproximatelyBenchmark.cs @@ -0,0 +1,25 @@ +using System; +using BenchmarkDotNet.Attributes; + +namespace Light.GuardClauses.Performance.ComparableAssertions; + +[MemoryDiagnoser] +// ReSharper disable once ClassCanBeSealed.Global -- Benchmark.NET derives from this class with dynamically created code +public class MustNotBeApproximatelyBenchmark +{ + [Benchmark] + public double MustNotBeApproximately() => 5.2.MustNotBeApproximately(5.1, 0.5); + + [Benchmark(Baseline = true)] + public double Imperative() => Imperative(5.2, 5.1, 0.5); + + private static double Imperative(double parameter, double other, double tolerance) + { + if (Math.Abs(parameter - other) < tolerance) + { + throw new ArgumentOutOfRangeException(nameof(parameter), $"Value is approximately {other}"); + } + + return parameter; + } +} \ No newline at end of file diff --git a/Code/Light.GuardClauses.Tests/ComparableAssertions/MustNotBeApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustNotBeApproximatelyTests.cs new file mode 100644 index 0000000..da9e7ea --- /dev/null +++ b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustNotBeApproximatelyTests.cs @@ -0,0 +1,238 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Light.GuardClauses.Tests.ComparableAssertions; + +public static class MustNotBeApproximatelyTests +{ + [Theory] + [InlineData(5.3, 5.0, 0.2)] + [InlineData(10.4, 10.3, 0.01)] + [InlineData(3.15, 3.14, 0.001)] + [InlineData(-42.002, -42.0001, 0.001)] + public static void ValuesNotApproximatelyEqual_Double(double value, double other, double tolerance) => + value.MustNotBeApproximately(other, tolerance).Should().Be(value); + + [Theory] + [InlineData(5.3f, 5.0f, 0.2f)] + [InlineData(10.4f, 10.3f, 0.01f)] + [InlineData(3.15f, 3.14f, 0.001f)] + [InlineData(-42.002f, -42.0001f, 0.001f)] + public static void ValuesNotApproximatelyEqual_Float(float value, float other, float tolerance) => + value.MustNotBeApproximately(other, tolerance).Should().Be(value); + + [Theory] + [InlineData(5.0, 5.05, 0.1)] + [InlineData(100.0, 99.95, 0.1)] + [InlineData(-20.0, -20.05, 0.1)] + [InlineData(0.0001, 0.00015, 0.0001)] + public static void ValuesApproximatelyEqual_Double(double value, double other, double tolerance) + { + var act = () => value.MustNotBeApproximately(other, tolerance, nameof(value)); + + var exceptionAssertion = act.Should().Throw().Which; + exceptionAssertion.Message.Should().Contain( + $"{nameof(value)} must not be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}." + ); + exceptionAssertion.ParamName.Should().BeSameAs(nameof(value)); + } + + [Theory] + [InlineData(5.0f, 5.05f, 0.1f)] + [InlineData(100.0f, 99.95f, 0.1f)] + [InlineData(-20.0f, -20.05f, 0.1f)] + [InlineData(0.0001f, 0.00015f, 0.0001f)] + public static void ValuesApproximatelyEqual_Float(float value, float other, float tolerance) + { + var act = () => value.MustNotBeApproximately(other, tolerance, nameof(value)); + + var exceptionAssertion = act.Should().Throw().Which; + exceptionAssertion.Message.Should().Contain( + $"{nameof(value)} must not be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}." + ); + exceptionAssertion.ParamName.Should().BeSameAs(nameof(value)); + } + + [Fact] + public static void DefaultTolerance_Double() + { + // Should throw - difference is 0.00005 which is less than default tolerance 0.0001 + const double value = 1.00005; + Action act = () => value.MustNotBeApproximately(1.0, "parameter"); + act.Should().Throw() + .WithParameterName("parameter"); + + // Should pass - difference is 0.0002 which is greater than default tolerance 0.0001 + const double value2 = 1.0002; + value2.MustNotBeApproximately(1.0).Should().Be(value2); + } + + [Fact] + public static void DefaultTolerance_Float() + { + // Should throw - difference is 0.00005f which is less than default tolerance 0.0001f + const float value = 1.00005f; + Action act = () => value.MustNotBeApproximately(1.0f, "parameter"); + act.Should().Throw() + .WithParameterName("parameter"); + + // Should pass - difference is 0.0002f which is greater than default tolerance 0.0001f + const float value2 = 1.0002f; + value2.MustNotBeApproximately(1.0f).Should().Be(value2); + } + + [Fact] + public static void CustomException_Double() => + Test.CustomException( + 5.0, + 5.0000000001, + (x, y, exceptionFactory) => x.MustNotBeApproximately(y, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionWithTolerance_Double() => + Test.CustomException( + 5.0, + 5.05, + 0.1, + (x, y, z, exceptionFactory) => x.MustNotBeApproximately(y, z, exceptionFactory) + ); + + [Fact] + public static void CustomException_Float() => + Test.CustomException( + 5.0f, + 5.000001f, + (x, y, exceptionFactory) => x.MustNotBeApproximately(y, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionWithTolerance_Float() => + Test.CustomException( + 5.0f, + 5.05f, + 0.1f, + (x, y, z, exceptionFactory) => x.MustNotBeApproximately(y, z, exceptionFactory) + ); + + [Fact] + public static void NoCustomExceptionThrown_Double() => + 5.2.MustNotBeApproximately(5.0, 0.1, (_, _, _) => null).Should().Be(5.2); + + [Fact] + public static void NoCustomExceptionThrown_Float() => + 5.2f.MustNotBeApproximately(5.0f, 0.1f, (_, _, _) => null).Should().Be(5.2f); + + [Fact] + public static void CustomMessage_Double() => + Test.CustomMessage( + message => 100.0.MustNotBeApproximately(100.05, 0.1, message: message) + ); + + [Fact] + public static void CustomMessage_Float() => + Test.CustomMessage( + message => 100.0f.MustNotBeApproximately(100.05f, 0.1f, message: message) + ); + + [Fact] + public static void CallerArgumentExpression_Double() + { + const double seventyEightO1 = 78.1; + + var act = () => seventyEightO1.MustNotBeApproximately(78.099999); + + act.Should().Throw() + .WithParameterName(nameof(seventyEightO1)); + } + + [Fact] + public static void CallerArgumentExpressionWithTolerance_Double() + { + const double pi = 3.14159; + + var act = () => pi.MustNotBeApproximately(3.14, 0.01); + + act.Should().Throw() + .WithParameterName(nameof(pi)); + } + + [Fact] + public static void CallerArgumentExpression_Float() + { + const float seventyEightO1 = 78.1f; + + var act = () => seventyEightO1.MustNotBeApproximately(78.100005f); + + act.Should().Throw() + .WithParameterName(nameof(seventyEightO1)); + } + + [Fact] + public static void CallerArgumentExpressionWithTolerance_Float() + { + const float pi = 3.14159f; + + var act = () => pi.MustNotBeApproximately(3.14f, 0.01f); + + act.Should().Throw() + .WithParameterName(nameof(pi)); + } + +#if NET8_0 + [Theory] + [InlineData(5.3, 5.0, 0.2)] + [InlineData(10.4, 10.3, 0.01)] + [InlineData(3.15, 3.14, 0.001)] + [InlineData(-42.002, -42.0001, 0.001)] + public static void ValuesNotApproximatelyEqual_Generic(double value, double other, double tolerance) => + value.MustNotBeApproximately(other, tolerance).Should().Be(value); + + [Theory] + [InlineData(5.0, 5.05, 0.1)] + [InlineData(100.0, 99.95, 0.1)] + [InlineData(-20.0, -20.05, 0.1)] + [InlineData(0.0001, 0.00015, 0.0001)] + public static void ValuesApproximatelyEqual_Generic(double value, double other, double tolerance) + { + var act = () => value.MustNotBeApproximately(other, tolerance, nameof(value)); + + var exceptionAssertion = act.Should().Throw().Which; + exceptionAssertion.Message.Should().Contain( + $"{nameof(value)} must not be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}." + ); + exceptionAssertion.ParamName.Should().BeSameAs(nameof(value)); + } + + [Fact] + public static void CustomExceptionWithTolerance_Generic() => + Test.CustomException( + 5.0, + 5.05, + 0.1, + (x, y, t, exceptionFactory) => x.MustNotBeApproximately(y, t, exceptionFactory) + ); + + [Fact] + public static void NoCustomExceptionThrown_Generic() => + 5.2.MustNotBeApproximately(5.0, 0.1, (_, _, _) => null).Should().Be(5.2); + + [Fact] + public static void CustomMessage_Generic() => + Test.CustomMessage( + message => 100.0.MustNotBeApproximately(100.05, 0.1, message: message) + ); + + [Fact] + public static void CallerArgumentExpressionWithTolerance_Generic() + { + const double seventyEightO1 = 78.1; + + var act = () => seventyEightO1.MustNotBeApproximately(78.0, 0.2); + + act.Should().Throw() + .WithParameterName(nameof(seventyEightO1)); + } +#endif +} \ No newline at end of file diff --git a/Code/Light.GuardClauses/Check.MustNotBeApproximately.cs b/Code/Light.GuardClauses/Check.MustNotBeApproximately.cs new file mode 100644 index 0000000..909ffe0 --- /dev/null +++ b/Code/Light.GuardClauses/Check.MustNotBeApproximately.cs @@ -0,0 +1,297 @@ +using System; +using System.Runtime.CompilerServices; +using Light.GuardClauses.ExceptionFactory; +#if NET8_0 +using System.Numerics; +#endif + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the specified is not approximately equal to the given + /// value, using the default tolerance of 0.0001, or otherwise throws an + /// . + /// + /// The value to be checked. + /// The value that should not be approximately equal to. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when the absolute difference between and is + /// less than 0.0001. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustNotBeApproximately( + this double parameter, + double other, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) => + parameter.MustNotBeApproximately(other, 0.0001, parameterName, message); + + /// + /// Ensures that the specified is not approximately equal to the given + /// value, using the default tolerance of 0.0001, or otherwise throws an + /// . + /// + /// The value to be checked. + /// The value that should not be approximately equal to. + /// + /// The delegate that creates your custom exception. and + /// are passed to this delegate. + /// + /// + /// Thrown when the absolute difference between and is + /// less than 0.0001. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustNotBeApproximately( + this double parameter, + double other, + Func exceptionFactory + ) + { + if (parameter.IsApproximately(other)) + { + Throw.CustomException(exceptionFactory, parameter, other); + } + + return parameter; + } + + /// + /// Ensures that the specified is not approximately equal to the given + /// value, or otherwise throws an . + /// + /// The value to be checked. + /// The value that should not be approximately equal to. + /// The tolerance indicating how much the two values may differ from each other. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when the absolute difference between and is + /// less than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustNotBeApproximately( + this double parameter, + double other, + double tolerance, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + if (parameter.IsApproximately(other, tolerance)) + { + Throw.MustNotBeApproximately(parameter, other, tolerance, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is not approximately equal to the given + /// value, or otherwise throws your custom exception. + /// + /// The value to be checked. + /// The value that should not be approximately equal to. + /// The tolerance indicating how much the two values may differ from each other. + /// The delegate that creates your custom exception. , + /// , and are passed to this delegate. + /// + /// Your custom exception thrown when the absolute difference between and + /// is less than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustNotBeApproximately( + this double parameter, + double other, + double tolerance, + Func exceptionFactory + ) + { + if (parameter.IsApproximately(other, tolerance)) + { + Throw.CustomException(exceptionFactory, parameter, other, tolerance); + } + + return parameter; + } + + /// + /// Ensures that the specified is not approximately equal to the given + /// value, using the default tolerance of 0.0001f, or otherwise throws an + /// . + /// + /// The value to be checked. + /// The value that should not be approximately equal to. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when the absolute difference between and is + /// less than 0.0001f. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustNotBeApproximately( + this float parameter, + float other, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + parameter.MustNotBeApproximately(other, 0.0001f, parameterName, message); + + /// + /// Ensures that the specified is not approximately equal to the given + /// value, using the default tolerance of 0.0001, or otherwise throws an + /// . + /// + /// The value to be checked. + /// The value that should not be approximately equal to. + /// + /// The delegate that creates your custom exception. and + /// are passed to this delegate. + /// + /// + /// Thrown when the absolute difference between and is + /// less than 0.0001. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustNotBeApproximately( + this float parameter, + float other, + Func exceptionFactory + ) + { + if (parameter.IsApproximately(other)) + { + Throw.CustomException(exceptionFactory, parameter, other); + } + + return parameter; + } + + /// + /// Ensures that the specified is not approximately equal to the given + /// value, or otherwise throws an . + /// + /// The value to be checked. + /// The value that should not be approximately equal to. + /// The tolerance indicating how much the two values may differ from each other. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when the absolute difference between and is + /// less than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustNotBeApproximately( + this float parameter, + float other, + float tolerance, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (parameter.IsApproximately(other, tolerance)) + { + Throw.MustNotBeApproximately(parameter, other, tolerance, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is not approximately equal to the given + /// value, or otherwise throws your custom exception. + /// + /// The value to be checked. + /// The value that should not be approximately equal to. + /// The tolerance indicating how much the two values may differ from each other. + /// + /// The delegate that creates your custom exception. , , and + /// are passed to this delegate. + /// + /// + /// Your custom exception thrown when the absolute difference between and + /// is less than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustNotBeApproximately( + this float parameter, + float other, + float tolerance, + Func exceptionFactory + ) + { + if (parameter.IsApproximately(other, tolerance)) + { + Throw.CustomException(exceptionFactory, parameter, other, tolerance); + } + + return parameter; + } + +#if NET8_0 + /// + /// Ensures that the specified is not approximately equal to the given + /// value, or otherwise throws an . + /// + /// The value to be checked. + /// The value that should not be approximately equal to. + /// The tolerance indicating how much the two values may differ from each other. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The type that implements the interface. + /// + /// Thrown when the absolute difference between and is + /// less than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T MustNotBeApproximately( + this T parameter, + T other, + T tolerance, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) where T : INumber + { + if (parameter.IsApproximately(other, tolerance)) + { + Throw.MustNotBeApproximately(parameter, other, tolerance, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is not approximately equal to the given + /// value, or otherwise throws your custom exception. + /// + /// The value to be checked. + /// The value that should not be approximately equal to. + /// The tolerance indicating how much the two values may differ from each other. + /// The delegate that creates your custom exception. , + /// , and are passed to this delegate. + /// The type that implements the interface. + /// + /// Your custom exception thrown when the absolute difference between and + /// is less than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T MustNotBeApproximately( + this T parameter, + T other, + T tolerance, + Func exceptionFactory + ) where T : INumber + { + if (parameter.IsApproximately(other, tolerance)) + { + Throw.CustomException(exceptionFactory, parameter, other, tolerance); + } + + return parameter; + } +#endif +} \ No newline at end of file diff --git a/Code/Light.GuardClauses/ExceptionFactory/Throw.MustNotBeApproximately.cs b/Code/Light.GuardClauses/ExceptionFactory/Throw.MustNotBeApproximately.cs new file mode 100644 index 0000000..3b8adfa --- /dev/null +++ b/Code/Light.GuardClauses/ExceptionFactory/Throw.MustNotBeApproximately.cs @@ -0,0 +1,28 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace Light.GuardClauses.ExceptionFactory; + +public static partial class Throw +{ + /// + /// Throws the default indicating that a value must not be approximately + /// equal to another value within a specified tolerance, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void MustNotBeApproximately( + T parameter, + T other, + T tolerance, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + throw new ArgumentOutOfRangeException( + parameterName, + message ?? + $"{parameterName ?? "The value"} must not be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {parameter}." + ); +} \ No newline at end of file