From 7d8f0d99e072078f4951e9864257a84a1b2f6651 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sat, 8 Mar 2025 09:49:35 +0100 Subject: [PATCH 1/6] chore: add comment to clarify index usage in IsFileExtension method Signed-off-by: Kenny Pflug --- Code/Light.GuardClauses/Check.IsFileExtension.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Code/Light.GuardClauses/Check.IsFileExtension.cs b/Code/Light.GuardClauses/Check.IsFileExtension.cs index d9c76cc..4e81452 100644 --- a/Code/Light.GuardClauses/Check.IsFileExtension.cs +++ b/Code/Light.GuardClauses/Check.IsFileExtension.cs @@ -62,6 +62,7 @@ public static bool IsFileExtension([NotNullWhen(true)] this string? value) => [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsFileExtension(this ReadOnlySpan value) { + // ReSharper disable once UseIndexFromEndExpression -- cannot use index from end expression in .NET Standard 2.0 if (value.Length <= 1 || value[0] != '.' || value[value.Length - 1] == '.') { return false; From 38a924941befab180ae7adc565da46cbab07a113 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sat, 8 Mar 2025 10:17:09 +0100 Subject: [PATCH 2/6] fix: IsApproximately now uses <= operator instead of the < operator Signed-off-by: Kenny Pflug --- .../CommonAssertions/IsApproximatelyTests.cs | 20 ++++++++++++------- .../Check.IsApproximately.cs | 10 +++++----- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs index afbf5af..70db5a8 100644 --- a/Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs +++ b/Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs @@ -17,7 +17,8 @@ public static void DoubleWithDefaultTolerance(double first, double second, bool [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.0, 10.0, true)] + [InlineData(4.9998, 15.0, 10.0, false)] [InlineData(5.0, 15.0001, 10.0, false)] public static void DoubleWithCustomTolerance(double first, double second, double tolerance, bool expected) => first.IsApproximately(second, tolerance).Should().Be(expected); @@ -34,7 +35,8 @@ public static void FloatWithDefaultTolerance(float first, float second, bool exp [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.0f, 10.0f, true)] + [InlineData(4.99f, 15.0f, 10.0f, false)] [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); @@ -44,7 +46,8 @@ public static void FloatWithCustomTolerance(float first, float second, float tol [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.0, 10.0, true)] + [InlineData(5.0, 15.000001, 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); @@ -53,14 +56,16 @@ public static void GenericDoubleWithCustomTolerance(double first, double second, [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.0f, 10.0f, true)] + [InlineData(5.0f, 15.01f, 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, 15, 10, true)] + [InlineData(4, 15, 10, false)] [InlineData(-5, 5, 12, true)] [InlineData(-100, 100, 199, false)] [InlineData(42, 42, 1, true)] @@ -69,7 +74,8 @@ public static void GenericIntWithCustomTolerance(int first, int second, int tole [Theory] [InlineData(5L, 10L, 10L, true)] - [InlineData(5L, 15L, 10L, false)] + [InlineData(5L, 15L, 10L, true)] + [InlineData(5L, 16L, 10L, false)] [InlineData(-5L, 5L, 12L, true)] [InlineData(-100L, 100L, 199L, false)] [InlineData(42L, 42L, 1L, true)] @@ -91,7 +97,7 @@ bool expected { 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 }, + { 5.0m, 15.0m, 9.99m, false }, }; #endif } diff --git a/Code/Light.GuardClauses/Check.IsApproximately.cs b/Code/Light.GuardClauses/Check.IsApproximately.cs index 6ee6172..02ef101 100644 --- a/Code/Light.GuardClauses/Check.IsApproximately.cs +++ b/Code/Light.GuardClauses/Check.IsApproximately.cs @@ -20,7 +20,7 @@ public static partial class Check /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsApproximately(this double value, double other, double tolerance) => - Math.Abs(value - other) < tolerance; + Math.Abs(value - other) <= tolerance; /// /// Checks if the specified value is approximately the same as the other value, using the default tolerance of 0.0001. @@ -33,7 +33,7 @@ public static bool IsApproximately(this double value, double other, double toler /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsApproximately(this double value, double other) => - Math.Abs(value - other) < 0.0001; + Math.Abs(value - other) <= 0.0001; /// /// Checks if the specified value is approximately the same as the other value, using the given tolerance. @@ -47,7 +47,7 @@ public static bool IsApproximately(this double value, double other) => /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsApproximately(this float value, float other, float tolerance) => - Math.Abs(value - other) < tolerance; + Math.Abs(value - other) <= tolerance; /// /// Checks if the specified value is approximately the same as the other value, using the default tolerance of 0.0001f. @@ -60,7 +60,7 @@ 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; + Math.Abs(value - other) <= 0.0001f; #if NET8_0 /// @@ -76,6 +76,6 @@ public static bool IsApproximately(this float value, float other) => /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsApproximately(this T value, T other, T tolerance) where T : INumber => - T.Abs(value - other) < tolerance; + T.Abs(value - other) <= tolerance; #endif } From 4c5db07c86cfe4c46a4bdfa6526cf61ee737cf23 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sat, 8 Mar 2025 10:17:31 +0100 Subject: [PATCH 3/6] feat: add IsGreaterThanOrApproximately Signed-off-by: Kenny Pflug --- .../IsGreaterThanOrApproximatelyTests.cs | 57 ++++++++++++++++++- .../Check.IsGreaterThanOrApproximately.cs | 20 +++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/Code/Light.GuardClauses.Tests/CommonAssertions/IsGreaterThanOrApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/CommonAssertions/IsGreaterThanOrApproximatelyTests.cs index d7d59d5..96ccedc 100644 --- a/Code/Light.GuardClauses.Tests/CommonAssertions/IsGreaterThanOrApproximatelyTests.cs +++ b/Code/Light.GuardClauses.Tests/CommonAssertions/IsGreaterThanOrApproximatelyTests.cs @@ -36,4 +36,59 @@ public static void FloatWithDefaultTolerance(float first, float second, bool exp [InlineData(1.0f, 2.0f, 0.1f, false)] public static void FloatWIthCustomTolerance(float first, float second, float tolerance, bool expected) => first.IsGreaterThanOrApproximately(second, tolerance).Should().Be(expected); -} \ No newline at end of file + +#if NET8_0 + [Theory] + [InlineData(15.91, 15.9, 0.1, true)] + [InlineData(24.449, 24.45, 0.0001, false)] + [InlineData(-3.12, -3.2, 0.001, true)] + [InlineData(2.369, 2.37, 0.0005, false)] + [InlineData(15.0, 14.0, 0.1, true)] // Greater than case + [InlineData(14.95, 15.0, 0.1, true)] // Approximately equal case + public static void GenericDoubleWithCustomTolerance(double first, double second, double tolerance, bool expected) => + first.IsGreaterThanOrApproximately(second, tolerance).Should().Be(expected); + + [Theory] + [InlineData(2.0f, 1.0f, 0.1f, true)] + [InlineData(1.0f, 1.0f, 0.1f, true)] + [InlineData(1.0f, 1.1f, 0.01f, false)] + [InlineData(1.0f, 2.0f, 0.1f, false)] + [InlineData(2.1f, 2.0f, 0.01f, true)] // Greater than case + public static void GenericFloatWithCustomTolerance(float first, float second, float tolerance, bool expected) => + first.IsGreaterThanOrApproximately(second, tolerance).Should().Be(expected); + + [Theory] + [InlineData(10, 5, 1, true)] // Greater than case + [InlineData(5, 5, 1, true)] // Equal case + [InlineData(5, 6, 1, true)] // Approximately equal case + [InlineData(5, 7, 1, false)] // Not greater than or approximately equal case + public static void GenericIntWithCustomTolerance(int first, int second, int tolerance, bool expected) => + first.IsGreaterThanOrApproximately(second, tolerance).Should().Be(expected); + + [Theory] + [InlineData(10L, 5L, 1L, true)] // Greater than case + [InlineData(5L, 5L, 1L, true)] // Equal case + [InlineData(5L, 6L, 1L, true)] // Approximately equal case + [InlineData(5L, 7L, 1L, false)] // Not greater than or approximately equal case + public static void GenericLongWithCustomTolerance(long first, long second, long tolerance, bool expected) => + first.IsGreaterThanOrApproximately(second, tolerance).Should().Be(expected); + + [Theory] + [MemberData(nameof(DecimalTestData))] + public static void GenericDecimalWithCustomTolerance( + decimal first, + decimal second, + decimal tolerance, + bool expected + ) => + first.IsGreaterThanOrApproximately(second, tolerance).Should().Be(expected); + + public static TheoryData DecimalTestData() => new () + { + { 1.3m, 1.1m, 0.1m, true }, // Greater than case + { 1.1m, 1.1m, 0.1m, true }, // Equal case + { 1.0m, 1.1m, 0.2m, true }, // Approximately equal case + { 1.0m, 1.3m, 0.1m, false }, // Not greater than or approximately equal case + }; +#endif +} diff --git a/Code/Light.GuardClauses/Check.IsGreaterThanOrApproximately.cs b/Code/Light.GuardClauses/Check.IsGreaterThanOrApproximately.cs index caca671..487540b 100644 --- a/Code/Light.GuardClauses/Check.IsGreaterThanOrApproximately.cs +++ b/Code/Light.GuardClauses/Check.IsGreaterThanOrApproximately.cs @@ -1,4 +1,7 @@ using System.Runtime.CompilerServices; +#if NET8_0 +using System.Numerics; +#endif namespace Light.GuardClauses; @@ -57,4 +60,21 @@ public static bool IsGreaterThanOrApproximately(this float value, float other, f [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsGreaterThanOrApproximately(this float value, float other) => value > other || value.IsApproximately(other); + +#if NET8_0 + /// + /// Checks if the specified value is greater than or approximately the same as the other value, using the given tolerance. + /// + /// The first value to compare. + /// The second value to compare. + /// The tolerance indicating how much the two values may differ from each other. + /// The type that implements the interface. + /// + /// True if is greater than or if their absolute difference + /// is smaller than the given , otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsGreaterThanOrApproximately(this T value, T other, T tolerance) where T : INumber => + value > other || value.IsApproximately(other, tolerance); +#endif } From 041530476d7fe69033c09e55d79a3cd941d8800c Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sat, 8 Mar 2025 10:24:22 +0100 Subject: [PATCH 4/6] feat: add IsLessThanOrApproximately Signed-off-by: Kenny Pflug --- .../IsLessThanOrApproximatelyTests.cs | 52 +++++++++++++++++++ .../Check.IsLessThanOrApproximately.cs | 20 +++++++ 2 files changed, 72 insertions(+) diff --git a/Code/Light.GuardClauses.Tests/CommonAssertions/IsLessThanOrApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/CommonAssertions/IsLessThanOrApproximatelyTests.cs index c710ec2..6e774b8 100644 --- a/Code/Light.GuardClauses.Tests/CommonAssertions/IsLessThanOrApproximatelyTests.cs +++ b/Code/Light.GuardClauses.Tests/CommonAssertions/IsLessThanOrApproximatelyTests.cs @@ -36,4 +36,56 @@ public static void FloatWithDefaultTolerance(float first, float second, bool exp [InlineData(0f, -1f, 0.9f, false)] public static void FloatWithCustomTolerance(float first, float second, float tolerance, bool expected) => first.IsLessThanOrApproximately(second, tolerance).Should().Be(expected); + +#if NET8_0 + [Theory] + [InlineData(13.25, 13.5, 0.1, true)] // Less than case + [InlineData(13.5, 13.5, 0.1, true)] // Equal case + [InlineData(13.55, 13.5, 0.1, true)] // Approximately equal case + [InlineData(13.7, 13.5, 0.1, false)] // Not less than or approximately equal case + public static void GenericDoubleWithCustomTolerance(double first, double second, double tolerance, bool expected) => + first.IsLessThanOrApproximately(second, tolerance).Should().Be(expected); + + [Theory] + [InlineData(13.25f, 13.5f, 0.1f, true)] // Less than case + [InlineData(13.5f, 13.5f, 0.1f, true)] // Equal case + [InlineData(13.55f, 13.5f, 0.1f, true)] // Approximately equal case + [InlineData(13.7f, 13.5f, 0.1f, false)] // Not less than or approximately equal case + public static void GenericFloatWithCustomTolerance(float first, float second, float tolerance, bool expected) => + first.IsLessThanOrApproximately(second, tolerance).Should().Be(expected); + + [Theory] + [InlineData(5, 10, 1, true)] // Less than case + [InlineData(5, 5, 1, true)] // Equal case + [InlineData(6, 5, 1, true)] // Approximately equal case + [InlineData(7, 5, 1, false)] // Not less than or approximately equal case + public static void GenericIntWithCustomTolerance(int first, int second, int tolerance, bool expected) => + first.IsLessThanOrApproximately(second, tolerance).Should().Be(expected); + + [Theory] + [InlineData(5L, 10L, 1L, true)] // Less than case + [InlineData(5L, 5L, 1L, true)] // Equal case + [InlineData(6L, 5L, 1L, true)] // Approximately equal case + [InlineData(7L, 5L, 1L, false)] // Not less than or approximately equal case + public static void GenericLongWithCustomTolerance(long first, long second, long tolerance, bool expected) => + first.IsLessThanOrApproximately(second, tolerance).Should().Be(expected); + + [Theory] + [MemberData(nameof(DecimalTestData))] + public static void GenericDecimalWithCustomTolerance( + decimal first, + decimal second, + decimal tolerance, + bool expected + ) => + first.IsLessThanOrApproximately(second, tolerance).Should().Be(expected); + + public static TheoryData DecimalTestData() => new () + { + { 1.0m, 1.2m, 0.1m, true }, // Less than case + { 1.1m, 1.1m, 0.1m, true }, // Equal case + { 1.2m, 1.1m, 0.1m, true }, // Approximately equal case + { 1.3m, 1.1m, 0.1m, false }, // Not less than or approximately equal case + }; +#endif } \ No newline at end of file diff --git a/Code/Light.GuardClauses/Check.IsLessThanOrApproximately.cs b/Code/Light.GuardClauses/Check.IsLessThanOrApproximately.cs index c24c1a0..89b109b 100644 --- a/Code/Light.GuardClauses/Check.IsLessThanOrApproximately.cs +++ b/Code/Light.GuardClauses/Check.IsLessThanOrApproximately.cs @@ -1,4 +1,7 @@ using System.Runtime.CompilerServices; +#if NET8_0 +using System.Numerics; +#endif namespace Light.GuardClauses; @@ -57,4 +60,21 @@ public static bool IsLessThanOrApproximately(this float value, float other, floa [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsLessThanOrApproximately(this float value, float other) => value < other || value.IsApproximately(other); + +#if NET8_0 + /// + /// Checks if the specified value is less than or approximately the same as the other value, using the given tolerance. + /// + /// The first value to compare. + /// The second value to compare. + /// The tolerance indicating how much the two values may differ from each other. + /// The type that implements the interface. + /// + /// True if is less than or if their absolute difference + /// is smaller than the given , otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLessThanOrApproximately(this T value, T other, T tolerance) where T : INumber => + value < other || value.IsApproximately(other, tolerance); +#endif } From 444ca1a78238fbd52ecf3d0f07d710114a530de9 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sat, 8 Mar 2025 11:03:31 +0100 Subject: [PATCH 5/6] feat: add MustBeGreaterThanOrApproximately Signed-off-by: Kenny Pflug --- .../MustBeGreaterThanOrApproximatelyTests.cs | 295 ++++++++++++++++++ .../Check.MustBeGreaterThanOrApproximately.cs | 292 +++++++++++++++++ .../Throw.MustBeGreaterThanOrApproximately.cs | 28 ++ 3 files changed, 615 insertions(+) create mode 100644 Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeGreaterThanOrApproximatelyTests.cs create mode 100644 Code/Light.GuardClauses/Check.MustBeGreaterThanOrApproximately.cs create mode 100644 Code/Light.GuardClauses/ExceptionFactory/Throw.MustBeGreaterThanOrApproximately.cs diff --git a/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeGreaterThanOrApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeGreaterThanOrApproximatelyTests.cs new file mode 100644 index 0000000..ff19f60 --- /dev/null +++ b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeGreaterThanOrApproximatelyTests.cs @@ -0,0 +1,295 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Light.GuardClauses.Tests.ComparableAssertions; + +public static class CheckMustBeGreaterThanOrApproximatelyTests +{ + [Theory] + [InlineData(17.4, 17.3)] + [InlineData(19.9999999, 20.0)] + [InlineData(-5.49998, -5.5)] + [InlineData(0.0001, 0.0001)] + public static void EqualOrGreater_Double(double first, double second) => + first.MustBeGreaterThanOrApproximately(second).Should().Be(first); + + [Theory] + [InlineData(15.91, 15.9, 0.1)] + [InlineData(24.49999, 24.45, 0.0001)] + [InlineData(-3.12, -3.2, 0.001)] + [InlineData(2.369, 2.37, 0.05)] + public static void EqualOrGreaterWithTolerance_Double(double first, double second, double tolerance) => + first.MustBeGreaterThanOrApproximately(second, tolerance).Should().Be(first); + + [Theory] + [InlineData(100.225f, 100.2f)] + [InlineData(-5.9f, -5.900005f)] + [InlineData(0f, -0.02f)] + [InlineData(-0.00001f, 0f)] + public static void EqualOrGreater_Float(float first, float second) => + first.MustBeGreaterThanOrApproximately(second).Should().Be(first); + + [Theory] + [InlineData(2.0f, 1.0f, 0.1f)] + [InlineData(1.0f, 1.0f, 0.1f)] + [InlineData(1.01f, 1.1f, 0.1f)] + [InlineData(1.0f, 2.0f, 1.0f)] + public static void EqualOrGreaterWithTolerance_Float(float first, float second, float tolerance) => + first.MustBeGreaterThanOrApproximately(second, tolerance).Should().Be(first); + + [Theory] + [InlineData(5.0, 5.3, 0.1)] + [InlineData(100.0, 100.5, 0.1)] + [InlineData(-20.0, -19.8, 0.1)] + [InlineData(0.0001, 0.0003, 0.00005)] + public static void NotGreaterThanOrApproximately_Double(double value, double other, double tolerance) + { + var act = () => value.MustBeGreaterThanOrApproximately(other, tolerance, nameof(value)); + + var exceptionAssertion = act.Should().Throw().Which; + exceptionAssertion.Message.Should().Contain( + $"{nameof(value)} must be greater than or 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, 100.5f, 0.1f)] + [InlineData(-20.0f, -19.8f, 0.1f)] + [InlineData(0.0001f, 0.0003f, 0.00005f)] + public static void NotGreaterThanOrApproximately_Float(float value, float other, float tolerance) + { + var act = () => value.MustBeGreaterThanOrApproximately(other, tolerance, nameof(value)); + + var exceptionAssertion = act.Should().Throw().Which; + exceptionAssertion.Message.Should().Contain( + $"{nameof(value)} must be greater than or 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.MustBeGreaterThanOrApproximately(1.0).Should().Be(value); + + // Should throw - difference is 0.0002 which is greater than default tolerance 0.0001 + Action act = () => 0.9998.MustBeGreaterThanOrApproximately(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.MustBeGreaterThanOrApproximately(1.0f).Should().Be(value); + + // Should throw - difference is 0.0002f which is greater than default tolerance 0.0001f + Action act = () => 0.9998f.MustBeGreaterThanOrApproximately(1.0f, "parameter"); + act.Should().Throw() + .WithParameterName("parameter"); + } + + [Fact] + public static void CustomException_Double() => + Test.CustomException( + 5.0, + 5.3, + (x, y, exceptionFactory) => x.MustBeGreaterThanOrApproximately(y, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionWithTolerance_Double() => + Test.CustomException( + 5.0, + 5.5, + 0.1, + (x, y, z, exceptionFactory) => x.MustBeGreaterThanOrApproximately(y, z, exceptionFactory) + ); + + [Fact] + public static void CustomException_Float() => + Test.CustomException( + 5.0f, + 5.3f, + (x, y, exceptionFactory) => x.MustBeGreaterThanOrApproximately(y, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionWithTolerance_Float() => + Test.CustomException( + 5.0f, + 5.5f, + 0.1f, + (x, y, z, exceptionFactory) => x.MustBeGreaterThanOrApproximately(y, z, exceptionFactory) + ); + + [Fact] + public static void NoCustomExceptionThrown_Double() => + 5.2.MustBeGreaterThanOrApproximately(5.1, (_, _) => null).Should().Be(5.2); + + [Fact] + public static void NoCustomExceptionThrownWithTolerance_Double() => + 5.2.MustBeGreaterThanOrApproximately(5.0, 0.1, (_, _, _) => null).Should().Be(5.2); + + [Fact] + public static void NoCustomExceptionThrown_Float() => + 5.2f.MustBeGreaterThanOrApproximately(5.0f, (_, _) => null).Should().Be(5.2f); + + [Fact] + public static void NoCustomExceptionThrownWithTolerance_Float() => + 5.2f.MustBeGreaterThanOrApproximately(5.0f, 0.1f, (_, _, _) => null).Should().Be(5.2f); + + [Fact] + public static void CustomMessage_Double() => + Test.CustomMessage( + message => 100.0.MustBeGreaterThanOrApproximately(101.0, 0.5, message: message) + ); + + [Fact] + public static void CustomMessage_Float() => + Test.CustomMessage( + message => 100.0f.MustBeGreaterThanOrApproximately(101.0f, 0.5f, message: message) + ); + + [Fact] + public static void CallerArgumentExpression_Double() + { + const double seventyEightO1 = 78.1; + + var act = () => seventyEightO1.MustBeGreaterThanOrApproximately(79.0); + + act.Should().Throw() + .WithParameterName(nameof(seventyEightO1)); + } + + [Fact] + public static void CallerArgumentExpressionWithTolerance_Double() + { + const double pi = 3.14159; + + var act = () => pi.MustBeGreaterThanOrApproximately(3.5, 0.1); + + act.Should().Throw() + .WithParameterName(nameof(pi)); + } + + [Fact] + public static void CallerArgumentExpression_Float() + { + const float seventyEightO1 = 78.1f; + + var act = () => seventyEightO1.MustBeGreaterThanOrApproximately(79.0f); + + act.Should().Throw() + .WithParameterName(nameof(seventyEightO1)); + } + + [Fact] + public static void CallerArgumentExpressionWithTolerance_Float() + { + const float pi = 3.14159f; + + var act = () => pi.MustBeGreaterThanOrApproximately(3.5f, 0.1f); + + act.Should().Throw() + .WithParameterName(nameof(pi)); + } + +#if NET8_0 + [Theory] + [InlineData(15.91, 15.9, 0.1)] + [InlineData(24.4999, 24.45, 0.0001)] + [InlineData(-3.12, -3.2, 0.001)] + [InlineData(2.369, 2.37, 0.05)] + [InlineData(15.0, 14.0, 0.1)] // Greater than case + [InlineData(14.95, 15.0, 0.1)] // Approximately equal case + public static void EqualOrGreaterWithCustomTolerance_GenericDouble(double first, double second, double tolerance) => + first.MustBeGreaterThanOrApproximately(second, tolerance).Should().Be(first); + + [Theory] + [InlineData(10, 5, 1)] // Greater than case + [InlineData(5, 5, 1)] // Equal case + [InlineData(5, 6, 1)] // Approximately equal case + [InlineData(5, 7, 2)] // Not greater than or approximately equal case + public static void EqualOrGreaterWithCustomTolerance_GenericInt32(int first, int second, int tolerance) => + first.MustBeGreaterThanOrApproximately(second, tolerance).Should().Be(first); + + [Theory] + [InlineData(10L, 5L, 1L)] // Greater than case + [InlineData(5L, 5L, 1L)] // Equal case + [InlineData(5L, 6L, 1L)] // Approximately equal case + [InlineData(4L, 7L, 3L)] // Not greater than or approximately equal case + public static void EqualOrGreaterWithCustomTolerance_GenericInt64(long first, long second, long tolerance) => + first.MustBeGreaterThanOrApproximately(second, tolerance).Should().Be(first); + + [Theory] + [MemberData(nameof(DecimalTestData))] + public static void GenericDecimalWithCustomTolerance( + decimal first, + decimal second, + decimal tolerance + ) => + first.MustBeGreaterThanOrApproximately(second, tolerance).Should().Be(first); + + public static TheoryData DecimalTestData() => new () + { + { 1.3m, 1.1m, 0.1m }, // Greater than case + { 1.1m, 1.1m, 0.1m }, // Equal case + { 1.0m, 1.1m, 0.2m }, // Approximately equal case + { 1.292m, 1.3m, 0.1m }, // Not greater than or approximately equal case + }; + + [Theory] + [InlineData(5.0, 5.3, 0.1)] + [InlineData(100.0, 100.5, 0.1)] + [InlineData(-20.0, -19.8, 0.1)] + [InlineData(0.0001, 0.0003, 0.00005)] + public static void NotGreaterThanOrApproximately_Generic(double value, double other, double tolerance) + { + var act = () => value.MustBeGreaterThanOrApproximately(other, tolerance, nameof(value)); + + var exceptionAssertion = act.Should().Throw().Which; + exceptionAssertion.Message.Should().Contain( + $"{nameof(value)} must be greater than or 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.5, + 0.1, + (x, y, t, exceptionFactory) => x.MustBeGreaterThanOrApproximately(y, t, exceptionFactory) + ); + + [Fact] + public static void NoCustomExceptionThrown_Generic() => + 5.2.MustBeGreaterThanOrApproximately(5.0, 0.1, (_, _, _) => null).Should().Be(5.2); + + [Fact] + public static void CustomMessage_Generic() => + Test.CustomMessage( + message => 100.0.MustBeGreaterThanOrApproximately(101.0, 0.5, message: message) + ); + + [Fact] + public static void CallerArgumentExpressionWithTolerance_Generic() + { + const double seventyEightO1 = 78.1; + + var act = () => seventyEightO1.MustBeGreaterThanOrApproximately(79.0, 0.5); + + act.Should().Throw() + .WithParameterName(nameof(seventyEightO1)); + } +#endif +} diff --git a/Code/Light.GuardClauses/Check.MustBeGreaterThanOrApproximately.cs b/Code/Light.GuardClauses/Check.MustBeGreaterThanOrApproximately.cs new file mode 100644 index 0000000..a0cd395 --- /dev/null +++ b/Code/Light.GuardClauses/Check.MustBeGreaterThanOrApproximately.cs @@ -0,0 +1,292 @@ +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 greater than or 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 greater than or approximately equal to. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is not greater than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeGreaterThanOrApproximately( + this double parameter, + double other, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) => + parameter.MustBeGreaterThanOrApproximately(other, 0.0001, parameterName, message); + + /// + /// Ensures that the specified is greater than or 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 greater than or approximately equal to. + /// + /// The delegate that creates your custom exception. and + /// are passed to this delegate. + /// + /// + /// Thrown when is not greater than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeGreaterThanOrApproximately( + this double parameter, + double other, + Func exceptionFactory + ) + { + if (!parameter.IsGreaterThanOrApproximately(other)) + { + Throw.CustomException(exceptionFactory, parameter, other); + } + + return parameter; + } + + /// + /// Ensures that the specified is greater than or approximately equal to the given + /// value, or otherwise throws an . + /// + /// The value to be checked. + /// The value that should be greater than or 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 is not greater than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeGreaterThanOrApproximately( + this double parameter, + double other, + double tolerance, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsGreaterThanOrApproximately(other, tolerance)) + { + Throw.MustBeGreaterThanOrApproximately(parameter, other, tolerance, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is greater than or approximately equal to the given + /// value, or otherwise throws your custom exception. + /// + /// The value to be checked. + /// The value that should be greater than or 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 is not greater than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeGreaterThanOrApproximately( + this double parameter, + double other, + double tolerance, + Func exceptionFactory + ) + { + if (!parameter.IsGreaterThanOrApproximately(other, tolerance)) + { + Throw.CustomException(exceptionFactory, parameter, other, tolerance); + } + + return parameter; + } + + /// + /// Ensures that the specified is greater than or 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 greater than or approximately equal to. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is not greater than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeGreaterThanOrApproximately( + this float parameter, + float other, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + parameter.MustBeGreaterThanOrApproximately(other, 0.0001f, parameterName, message); + + /// + /// Ensures that the specified is greater than or 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 greater than or approximately equal to. + /// + /// The delegate that creates your custom exception. and + /// are passed to this delegate. + /// + /// + /// Thrown when is not greater than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeGreaterThanOrApproximately( + this float parameter, + float other, + Func exceptionFactory + ) + { + if (!parameter.IsGreaterThanOrApproximately(other)) + { + Throw.CustomException(exceptionFactory, parameter, other); + } + + return parameter; + } + + /// + /// Ensures that the specified is greater than or approximately equal to the given + /// value, or otherwise throws an . + /// + /// The value to be checked. + /// The value that should be greater than or 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 is not greater than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeGreaterThanOrApproximately( + this float parameter, + float other, + float tolerance, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsGreaterThanOrApproximately(other, tolerance)) + { + Throw.MustBeGreaterThanOrApproximately(parameter, other, tolerance, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is greater than or approximately equal to the given + /// value, or otherwise throws your custom exception. + /// + /// The value to be checked. + /// The value that should be greater than or 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 is not greater than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeGreaterThanOrApproximately( + this float parameter, + float other, + float tolerance, + Func exceptionFactory + ) + { + if (!parameter.IsGreaterThanOrApproximately(other, tolerance)) + { + Throw.CustomException(exceptionFactory, parameter, other, tolerance); + } + + return parameter; + } + +#if NET8_0 + /// + /// Ensures that the specified is greater than or approximately equal to the given + /// value, or otherwise throws an . + /// + /// The value to be checked. + /// The value that should be greater than or 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 is not greater than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T MustBeGreaterThanOrApproximately( + this T parameter, + T other, + T tolerance, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) where T : INumber + { + if (!parameter.IsGreaterThanOrApproximately(other, tolerance)) + { + Throw.MustBeGreaterThanOrApproximately(parameter, other, tolerance, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is greater than or approximately equal to the given + /// value, or otherwise throws your custom exception. + /// + /// The value to be checked. + /// The value that should be greater than or 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 is not greater than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T MustBeGreaterThanOrApproximately( + this T parameter, + T other, + T tolerance, + Func exceptionFactory + ) where T : INumber + { + if (!parameter.IsGreaterThanOrApproximately(other, tolerance)) + { + Throw.CustomException(exceptionFactory, parameter, other, tolerance); + } + + return parameter; + } +#endif +} diff --git a/Code/Light.GuardClauses/ExceptionFactory/Throw.MustBeGreaterThanOrApproximately.cs b/Code/Light.GuardClauses/ExceptionFactory/Throw.MustBeGreaterThanOrApproximately.cs new file mode 100644 index 0000000..a67a571 --- /dev/null +++ b/Code/Light.GuardClauses/ExceptionFactory/Throw.MustBeGreaterThanOrApproximately.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 greater than or approximately + /// equal to another value within a specified tolerance, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void MustBeGreaterThanOrApproximately( + T parameter, + T other, + T tolerance, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + throw new ArgumentOutOfRangeException( + parameterName, + message ?? + $"{parameterName ?? "The value"} must be greater than or approximately equal to {other} with a tolerance of {tolerance}, but it actually is {parameter}." + ); +} From b02763a25ea7ef067c57ea26250b330325cad104 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sat, 8 Mar 2025 11:28:18 +0100 Subject: [PATCH 6/6] feat: add MustBeLessThanOrApproximately Signed-off-by: Kenny Pflug --- .../MustBeLessThanOrApproximatelyTests.cs | 295 ++++++++++++++++++ .../Check.MustBeLessThanOrApproximately.cs | 292 +++++++++++++++++ .../Throw.MustBeLessThanOrApproximately.cs | 28 ++ 3 files changed, 615 insertions(+) create mode 100644 Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeLessThanOrApproximatelyTests.cs create mode 100644 Code/Light.GuardClauses/Check.MustBeLessThanOrApproximately.cs create mode 100644 Code/Light.GuardClauses/ExceptionFactory/Throw.MustBeLessThanOrApproximately.cs diff --git a/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeLessThanOrApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeLessThanOrApproximatelyTests.cs new file mode 100644 index 0000000..9eace40 --- /dev/null +++ b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeLessThanOrApproximatelyTests.cs @@ -0,0 +1,295 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Light.GuardClauses.Tests.ComparableAssertions; + +public static class MustBeLessThanOrApproximatelyTests +{ + [Theory] + [InlineData(17.3, 17.4)] + [InlineData(19.9999999, 20.0)] + [InlineData(-5.5, -5.49998)] + [InlineData(0.0001, 0.0001)] + public static void EqualOrLess_Double(double first, double second) => + first.MustBeLessThanOrApproximately(second).Should().Be(first); + + [Theory] + [InlineData(15.9, 15.91, 0.1)] + [InlineData(24.45, 24.49999, 0.0001)] + [InlineData(-3.2, -3.12, 0.001)] + [InlineData(2.37, 2.369, 0.05)] + public static void EqualOrLessWithTolerance_Double(double first, double second, double tolerance) => + first.MustBeLessThanOrApproximately(second, tolerance).Should().Be(first); + + [Theory] + [InlineData(100.2f, 100.225f)] + [InlineData(-5.900005f, -5.9f)] + [InlineData(-0.02f, 0f)] + [InlineData(0f, 0.00001f)] + public static void EqualOrLess_Float(float first, float second) => + first.MustBeLessThanOrApproximately(second).Should().Be(first); + + [Theory] + [InlineData(1.0f, 2.0f, 0.1f)] + [InlineData(1.0f, 1.0f, 0.1f)] + [InlineData(1.1f, 1.01f, 0.1f)] + [InlineData(1.0f, 2.0f, 1.0f)] + public static void EqualOrLessWithTolerance_Float(float first, float second, float tolerance) => + first.MustBeLessThanOrApproximately(second, tolerance).Should().Be(first); + + [Theory] + [InlineData(5.3, 5.0, 0.1)] + [InlineData(100.5, 100.0, 0.1)] + [InlineData(-19.8, -20.0, 0.1)] + [InlineData(0.0003, 0.0001, 0.00005)] + public static void NotLessThanOrApproximately_Double(double value, double other, double tolerance) + { + var act = () => value.MustBeLessThanOrApproximately(other, tolerance, nameof(value)); + + var exceptionAssertion = act.Should().Throw().Which; + exceptionAssertion.Message.Should().Contain( + $"{nameof(value)} must be less than or approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}." + ); + exceptionAssertion.ParamName.Should().BeSameAs(nameof(value)); + } + + [Theory] + [InlineData(5.3f, 5.0f, 0.1f)] + [InlineData(100.5f, 100.0f, 0.1f)] + [InlineData(-19.8f, -20.0f, 0.1f)] + [InlineData(0.0003f, 0.0001f, 0.00005f)] + public static void NotLessThanOrApproximately_Float(float value, float other, float tolerance) + { + var act = () => value.MustBeLessThanOrApproximately(other, tolerance, nameof(value)); + + var exceptionAssertion = act.Should().Throw().Which; + exceptionAssertion.Message.Should().Contain( + $"{nameof(value)} must be less than or 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.0; + value.MustBeLessThanOrApproximately(1.00005).Should().Be(value); + + // Should throw - difference is 0.0002 which is greater than default tolerance 0.0001 + Action act = () => 1.0002.MustBeLessThanOrApproximately(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.0f; + value.MustBeLessThanOrApproximately(1.00005f).Should().Be(value); + + // Should throw - difference is 0.0002f which is greater than default tolerance 0.0001f + Action act = () => 1.0002f.MustBeLessThanOrApproximately(1.0f, "parameter"); + act.Should().Throw() + .WithParameterName("parameter"); + } + + [Fact] + public static void CustomException_Double() => + Test.CustomException( + 5.3, + 5.0, + (x, y, exceptionFactory) => x.MustBeLessThanOrApproximately(y, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionWithTolerance_Double() => + Test.CustomException( + 5.5, + 5.0, + 0.1, + (x, y, z, exceptionFactory) => x.MustBeLessThanOrApproximately(y, z, exceptionFactory) + ); + + [Fact] + public static void CustomException_Float() => + Test.CustomException( + 5.3f, + 5.0f, + (x, y, exceptionFactory) => x.MustBeLessThanOrApproximately(y, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionWithTolerance_Float() => + Test.CustomException( + 5.5f, + 5.0f, + 0.1f, + (x, y, z, exceptionFactory) => x.MustBeLessThanOrApproximately(y, z, exceptionFactory) + ); + + [Fact] + public static void NoCustomExceptionThrown_Double() => + 5.1.MustBeLessThanOrApproximately(5.2, (_, _) => null).Should().Be(5.1); + + [Fact] + public static void NoCustomExceptionThrownWithTolerance_Double() => + 5.0.MustBeLessThanOrApproximately(5.2, 0.1, (_, _, _) => null).Should().Be(5.0); + + [Fact] + public static void NoCustomExceptionThrown_Float() => + 5.0f.MustBeLessThanOrApproximately(5.2f, (_, _) => null).Should().Be(5.0f); + + [Fact] + public static void NoCustomExceptionThrownWithTolerance_Float() => + 5.0f.MustBeLessThanOrApproximately(5.2f, 0.1f, (_, _, _) => null).Should().Be(5.0f); + + [Fact] + public static void CustomMessage_Double() => + Test.CustomMessage( + message => 101.0.MustBeLessThanOrApproximately(100.0, 0.5, message: message) + ); + + [Fact] + public static void CustomMessage_Float() => + Test.CustomMessage( + message => 101.0f.MustBeLessThanOrApproximately(100.0f, 0.5f, message: message) + ); + + [Fact] + public static void CallerArgumentExpression_Double() + { + const double seventyNinePoint0 = 79.0; + + var act = () => seventyNinePoint0.MustBeLessThanOrApproximately(78.1); + + act.Should().Throw() + .WithParameterName(nameof(seventyNinePoint0)); + } + + [Fact] + public static void CallerArgumentExpressionWithTolerance_Double() + { + const double threePointFive = 3.5; + + var act = () => threePointFive.MustBeLessThanOrApproximately(3.14159, 0.1); + + act.Should().Throw() + .WithParameterName(nameof(threePointFive)); + } + + [Fact] + public static void CallerArgumentExpression_Float() + { + const float seventyNinePoint0 = 79.0f; + + var act = () => seventyNinePoint0.MustBeLessThanOrApproximately(78.1f); + + act.Should().Throw() + .WithParameterName(nameof(seventyNinePoint0)); + } + + [Fact] + public static void CallerArgumentExpressionWithTolerance_Float() + { + const float threePointFive = 3.5f; + + var act = () => threePointFive.MustBeLessThanOrApproximately(3.14159f, 0.1f); + + act.Should().Throw() + .WithParameterName(nameof(threePointFive)); + } + +#if NET8_0 + [Theory] + [InlineData(15.9, 15.91, 0.1)] + [InlineData(24.45, 24.4999, 0.0001)] + [InlineData(-3.2, -3.12, 0.001)] + [InlineData(2.37, 2.369, 0.05)] + [InlineData(14.0, 15.0, 0.1)] // Less than case + [InlineData(15.0, 14.95, 0.1)] // Approximately equal case + public static void EqualOrLessWithCustomTolerance_GenericDouble(double first, double second, double tolerance) => + first.MustBeLessThanOrApproximately(second, tolerance).Should().Be(first); + + [Theory] + [InlineData(5, 10, 1)] // Less than case + [InlineData(5, 5, 1)] // Equal case + [InlineData(6, 5, 1)] // Approximately equal case + [InlineData(7, 5, 2)] // Not less than or approximately equal case + public static void EqualOrLessWithCustomTolerance_GenericInt32(int first, int second, int tolerance) => + first.MustBeLessThanOrApproximately(second, tolerance).Should().Be(first); + + [Theory] + [InlineData(5L, 10L, 1L)] // Less than case + [InlineData(5L, 5L, 1L)] // Equal case + [InlineData(6L, 5L, 1L)] // Approximately equal case + [InlineData(7L, 4L, 3L)] // Not less than or approximately equal case + public static void EqualOrLessWithCustomTolerance_GenericInt64(long first, long second, long tolerance) => + first.MustBeLessThanOrApproximately(second, tolerance).Should().Be(first); + + [Theory] + [MemberData(nameof(DecimalTestData))] + public static void GenericDecimalWithCustomTolerance( + decimal first, + decimal second, + decimal tolerance + ) => + first.MustBeLessThanOrApproximately(second, tolerance).Should().Be(first); + + public static TheoryData DecimalTestData() => new () + { + { 1.1m, 1.3m, 0.1m }, // Less than case + { 1.1m, 1.1m, 0.1m }, // Equal case + { 1.1m, 1.0m, 0.2m }, // Approximately equal case + { 1.3m, 1.292m, 0.1m }, // Not less than or approximately equal case + }; + + [Theory] + [InlineData(5.3, 5.0, 0.1)] + [InlineData(100.5, 100.0, 0.1)] + [InlineData(-19.8, -20.0, 0.1)] + [InlineData(0.0003, 0.0001, 0.00005)] + public static void NotLessThanOrApproximately_Generic(double value, double other, double tolerance) + { + var act = () => value.MustBeLessThanOrApproximately(other, tolerance, nameof(value)); + + var exceptionAssertion = act.Should().Throw().Which; + exceptionAssertion.Message.Should().Contain( + $"{nameof(value)} must be less than or 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.5, + 5.0, + 0.1, + (x, y, t, exceptionFactory) => x.MustBeLessThanOrApproximately(y, t, exceptionFactory) + ); + + [Fact] + public static void NoCustomExceptionThrown_Generic() => + 5.0.MustBeLessThanOrApproximately(5.2, 0.1, (_, _, _) => null).Should().Be(5.0); + + [Fact] + public static void CustomMessage_Generic() => + Test.CustomMessage( + message => 101.0.MustBeLessThanOrApproximately(100.0, 0.5, message: message) + ); + + [Fact] + public static void CallerArgumentExpressionWithTolerance_Generic() + { + const double seventyNinePoint0 = 79.0; + + var act = () => seventyNinePoint0.MustBeLessThanOrApproximately(78.1, 0.5); + + act.Should().Throw() + .WithParameterName(nameof(seventyNinePoint0)); + } +#endif +} \ No newline at end of file diff --git a/Code/Light.GuardClauses/Check.MustBeLessThanOrApproximately.cs b/Code/Light.GuardClauses/Check.MustBeLessThanOrApproximately.cs new file mode 100644 index 0000000..f48bf5d --- /dev/null +++ b/Code/Light.GuardClauses/Check.MustBeLessThanOrApproximately.cs @@ -0,0 +1,292 @@ +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 less than or 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 less than or approximately equal to. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is not less than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeLessThanOrApproximately( + this double parameter, + double other, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) => + parameter.MustBeLessThanOrApproximately(other, 0.0001, parameterName, message); + + /// + /// Ensures that the specified is less than or 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 less than or approximately equal to. + /// + /// The delegate that creates your custom exception. and + /// are passed to this delegate. + /// + /// + /// Thrown when is not less than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeLessThanOrApproximately( + this double parameter, + double other, + Func exceptionFactory + ) + { + if (!parameter.IsLessThanOrApproximately(other)) + { + Throw.CustomException(exceptionFactory, parameter, other); + } + + return parameter; + } + + /// + /// Ensures that the specified is less than or approximately equal to the given + /// value, or otherwise throws an . + /// + /// The value to be checked. + /// The value that should be less than or 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 is not less than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeLessThanOrApproximately( + this double parameter, + double other, + double tolerance, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsLessThanOrApproximately(other, tolerance)) + { + Throw.MustBeLessThanOrApproximately(parameter, other, tolerance, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is less than or approximately equal to the given + /// value, or otherwise throws your custom exception. + /// + /// The value to be checked. + /// The value that should be less than or 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 is not less than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeLessThanOrApproximately( + this double parameter, + double other, + double tolerance, + Func exceptionFactory + ) + { + if (!parameter.IsLessThanOrApproximately(other, tolerance)) + { + Throw.CustomException(exceptionFactory, parameter, other, tolerance); + } + + return parameter; + } + + /// + /// Ensures that the specified is less than or 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 less than or approximately equal to. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is not less than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeLessThanOrApproximately( + this float parameter, + float other, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + parameter.MustBeLessThanOrApproximately(other, 0.0001f, parameterName, message); + + /// + /// Ensures that the specified is less than or 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 less than or approximately equal to. + /// + /// The delegate that creates your custom exception. and + /// are passed to this delegate. + /// + /// + /// Thrown when is not less than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeLessThanOrApproximately( + this float parameter, + float other, + Func exceptionFactory + ) + { + if (!parameter.IsLessThanOrApproximately(other)) + { + Throw.CustomException(exceptionFactory, parameter, other); + } + + return parameter; + } + + /// + /// Ensures that the specified is less than or approximately equal to the given + /// value, or otherwise throws an . + /// + /// The value to be checked. + /// The value that should be less than or 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 is not less than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeLessThanOrApproximately( + this float parameter, + float other, + float tolerance, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsLessThanOrApproximately(other, tolerance)) + { + Throw.MustBeLessThanOrApproximately(parameter, other, tolerance, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is less than or approximately equal to the given + /// value, or otherwise throws your custom exception. + /// + /// The value to be checked. + /// The value that should be less than or 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 is not less than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeLessThanOrApproximately( + this float parameter, + float other, + float tolerance, + Func exceptionFactory + ) + { + if (!parameter.IsLessThanOrApproximately(other, tolerance)) + { + Throw.CustomException(exceptionFactory, parameter, other, tolerance); + } + + return parameter; + } + +#if NET8_0 + /// + /// Ensures that the specified is less than or approximately equal to the given + /// value, or otherwise throws an . + /// + /// The value to be checked. + /// The value that should be less than or 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 is not less than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T MustBeLessThanOrApproximately( + this T parameter, + T other, + T tolerance, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) where T : INumber + { + if (!parameter.IsLessThanOrApproximately(other, tolerance)) + { + Throw.MustBeLessThanOrApproximately(parameter, other, tolerance, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is less than or approximately equal to the given + /// value, or otherwise throws your custom exception. + /// + /// The value to be checked. + /// The value that should be less than or 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 is not less than or approximately equal to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T MustBeLessThanOrApproximately( + this T parameter, + T other, + T tolerance, + Func exceptionFactory + ) where T : INumber + { + if (!parameter.IsLessThanOrApproximately(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.MustBeLessThanOrApproximately.cs b/Code/Light.GuardClauses/ExceptionFactory/Throw.MustBeLessThanOrApproximately.cs new file mode 100644 index 0000000..40827d8 --- /dev/null +++ b/Code/Light.GuardClauses/ExceptionFactory/Throw.MustBeLessThanOrApproximately.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 less than or approximately + /// equal to another value within a specified tolerance, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void MustBeLessThanOrApproximately( + T parameter, + T other, + T tolerance, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + throw new ArgumentOutOfRangeException( + parameterName, + message ?? + $"{parameterName ?? "The value"} must be less than or approximately equal to {other} with a tolerance of {tolerance}, but it actually is {parameter}." + ); +} \ No newline at end of file