Skip to content

Commit 20b52ce

Browse files
authored
Merge pull request #111 from feO2x/features/generic-is-approximately
Generic IsApproximately and MustBeApproximately/MustNotBeApproximately
2 parents c0eabaf + 66be15f commit 20b52ce

File tree

12 files changed

+1265
-16
lines changed

12 files changed

+1265
-16
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using BenchmarkDotNet.Attributes;
3+
4+
namespace Light.GuardClauses.Performance.ComparableAssertions;
5+
6+
[MemoryDiagnoser]
7+
// ReSharper disable once ClassCanBeSealed.Global -- Benchmark.NET derives from this class with dynamically created code
8+
public class MustBeApproximatelyBenchmark
9+
{
10+
[Benchmark]
11+
public double MustBeApproximately() => 5.100001.MustBeApproximately(5.100000, 0.0001);
12+
13+
[Benchmark(Baseline = true)]
14+
public double Imperative() => Imperative(5.100001, 5.100000, 0.0001);
15+
16+
private static double Imperative(double parameter, double other, double tolerance)
17+
{
18+
if (Math.Abs(parameter - other) > tolerance)
19+
{
20+
throw new ArgumentOutOfRangeException(nameof(parameter), $"Value is not approximately {other}");
21+
}
22+
23+
return parameter;
24+
}
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using BenchmarkDotNet.Attributes;
3+
4+
namespace Light.GuardClauses.Performance.ComparableAssertions;
5+
6+
[MemoryDiagnoser]
7+
// ReSharper disable once ClassCanBeSealed.Global -- Benchmark.NET derives from this class with dynamically created code
8+
public class MustNotBeApproximatelyBenchmark
9+
{
10+
[Benchmark]
11+
public double MustNotBeApproximately() => 5.2.MustNotBeApproximately(5.1, 0.5);
12+
13+
[Benchmark(Baseline = true)]
14+
public double Imperative() => Imperative(5.2, 5.1, 0.5);
15+
16+
private static double Imperative(double parameter, double other, double tolerance)
17+
{
18+
if (Math.Abs(parameter - other) < tolerance)
19+
{
20+
throw new ArgumentOutOfRangeException(nameof(parameter), $"Value is approximately {other}");
21+
}
22+
23+
return parameter;
24+
}
25+
}

Code/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
<ItemGroup>
1010
<ProjectReference Include="..\Light.GuardClauses\Light.GuardClauses.csproj" />
11-
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
11+
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
1212
</ItemGroup>
1313

1414
</Project>

Code/Light.GuardClauses.Performance/Program.cs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,17 @@
44
using BenchmarkDotNet.Jobs;
55
using BenchmarkDotNet.Running;
66

7-
namespace Light.GuardClauses.Performance
7+
namespace Light.GuardClauses.Performance;
8+
9+
public static class Program
810
{
9-
public static class Program
10-
{
11-
private static IConfig DefaultConfiguration =>
12-
DefaultConfig
13-
.Instance
14-
.AddJob(Job.Default.WithRuntime(CoreRuntime.Core70))
15-
.AddJob(Job.Default.WithRuntime(ClrRuntime.Net48))
16-
.AddDiagnoser(MemoryDiagnoser.Default, new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig()));
11+
private static IConfig DefaultConfiguration =>
12+
DefaultConfig
13+
.Instance
14+
.AddJob(Job.Default.WithRuntime(CoreRuntime.Core80))
15+
.AddJob(Job.Default.WithRuntime(ClrRuntime.Net48))
16+
.AddDiagnoser(MemoryDiagnoser.Default, new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig()));
1717

18-
public static void Main(string[] arguments) =>
19-
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(arguments, DefaultConfiguration);
20-
}
18+
public static void Main(string[] arguments) =>
19+
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(arguments, DefaultConfiguration);
2120
}

Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,60 @@ public static void FloatWithDefaultTolerance(float first, float second, bool exp
3838
[InlineData(5.0f, 15.0001f, 10.0f, false)]
3939
public static void FloatWithCustomTolerance(float first, float second, float tolerance, bool expected) =>
4040
first.IsApproximately(second, tolerance).Should().Be(expected);
41-
}
41+
42+
#if NET8_0
43+
[Theory]
44+
[InlineData(1.1, 1.3, 0.5, true)]
45+
[InlineData(100.55, 100.555, 0.00001, false)]
46+
[InlineData(5.0, 14.999999, 10.0, true)]
47+
[InlineData(5.0, 15.0, 10.0, false)]
48+
[InlineData(5.0, 15.0001, 10.0, false)]
49+
public static void GenericDoubleWithCustomTolerance(double first, double second, double tolerance, bool expected) =>
50+
first.IsApproximately<double>(second, tolerance).Should().Be(expected);
51+
52+
[Theory]
53+
[InlineData(1.1f, 1.3f, 0.5f, true)]
54+
[InlineData(100.55f, 100.555f, 0.00001f, false)]
55+
[InlineData(5.0f, 14.999999f, 10.0f, true)]
56+
[InlineData(5.0f, 15.0f, 10.0f, false)]
57+
[InlineData(5.0f, 15.0001f, 10.0f, false)]
58+
public static void GenericFloatWithCustomTolerance(float first, float second, float tolerance, bool expected) =>
59+
first.IsApproximately<float>(second, tolerance).Should().Be(expected);
60+
61+
[Theory]
62+
[InlineData(5, 10, 10, true)]
63+
[InlineData(5, 15, 10, false)]
64+
[InlineData(-5, 5, 12, true)]
65+
[InlineData(-100, 100, 199, false)]
66+
[InlineData(42, 42, 1, true)]
67+
public static void GenericIntWithCustomTolerance(int first, int second, int tolerance, bool expected) =>
68+
first.IsApproximately(second, tolerance).Should().Be(expected);
69+
70+
[Theory]
71+
[InlineData(5L, 10L, 10L, true)]
72+
[InlineData(5L, 15L, 10L, false)]
73+
[InlineData(-5L, 5L, 12L, true)]
74+
[InlineData(-100L, 100L, 199L, false)]
75+
[InlineData(42L, 42L, 1L, true)]
76+
public static void GenericLongWithCustomTolerance(long first, long second, long tolerance, bool expected) =>
77+
first.IsApproximately(second, tolerance).Should().Be(expected);
78+
79+
[Theory]
80+
[MemberData(nameof(DecimalTestData))]
81+
public static void GenericDecimalWithCustomTolerance(
82+
decimal first,
83+
decimal second,
84+
decimal tolerance,
85+
bool expected
86+
) =>
87+
first.IsApproximately(second, tolerance).Should().Be(expected);
88+
89+
public static TheoryData<decimal, decimal, decimal, bool> DecimalTestData() => new ()
90+
{
91+
{ 1.1m, 1.3m, 0.5m, true },
92+
{ 100.55m, 100.555m, 0.00001m, false },
93+
{ 5.0m, 14.999999m, 10.0m, true },
94+
{ 5.0m, 15.0m, 10.0m, false },
95+
};
96+
#endif
97+
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
using System;
2+
using FluentAssertions;
3+
using Xunit;
4+
5+
namespace Light.GuardClauses.Tests.ComparableAssertions;
6+
7+
public static class MustBeApproximatelyTests
8+
{
9+
[Theory]
10+
[InlineData(5.1, 5.0, 0.2)]
11+
[InlineData(10.3, 10.3, 0.01)]
12+
[InlineData(3.14159, 3.14, 0.002)]
13+
[InlineData(-42.0, -42.0001, 0.001)]
14+
public static void ValuesApproximatelyEqual_Double(double value, double other, double tolerance) =>
15+
value.MustBeApproximately(other, tolerance).Should().Be(value);
16+
17+
[Theory]
18+
[InlineData(5.1f, 5.0f, 0.2f)]
19+
[InlineData(10.3f, 10.3f, 0.01f)]
20+
[InlineData(3.14159f, 3.14f, 0.002f)]
21+
[InlineData(-42.0f, -42.0001f, 0.001f)]
22+
public static void ValuesApproximatelyEqual_Float(float value, float other, float tolerance) =>
23+
value.MustBeApproximately(other, tolerance).Should().Be(value);
24+
25+
[Theory]
26+
[InlineData(5.0, 5.3, 0.1)]
27+
[InlineData(100.0, 99.8, 0.1)]
28+
[InlineData(-20.0, -20.2, 0.1)]
29+
[InlineData(0.0001, 0.0002, 0.00005)]
30+
public static void ValuesNotApproximatelyEqual_Double(double value, double other, double tolerance)
31+
{
32+
var act = () => value.MustBeApproximately(other, tolerance, nameof(value));
33+
34+
var exceptionAssertion = act.Should().Throw<ArgumentOutOfRangeException>().Which;
35+
exceptionAssertion.Message.Should().Contain(
36+
$"{nameof(value)} must be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
37+
);
38+
exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
39+
}
40+
41+
[Theory]
42+
[InlineData(5.0f, 5.3f, 0.1f)]
43+
[InlineData(100.0f, 99.8f, 0.1f)]
44+
[InlineData(-20.0f, -20.2f, 0.1f)]
45+
[InlineData(0.0001f, 0.0002f, 0.00005f)]
46+
public static void ValuesNotApproximatelyEqual_Float(float value, float other, float tolerance)
47+
{
48+
var act = () => value.MustBeApproximately(other, tolerance, nameof(value));
49+
50+
var exceptionAssertion = act.Should().Throw<ArgumentOutOfRangeException>().Which;
51+
exceptionAssertion.Message.Should().Contain(
52+
$"{nameof(value)} must be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
53+
);
54+
exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
55+
}
56+
57+
[Fact]
58+
public static void DefaultTolerance_Double()
59+
{
60+
// Should pass - difference is 0.00005 which is less than default tolerance 0.0001
61+
const double value = 1.00005;
62+
value.MustBeApproximately(1.0).Should().Be(value);
63+
64+
// Should throw - difference is 0.0002 which is greater than default tolerance 0.0001
65+
Action act = () => 1.0002.MustBeApproximately(1.0, "parameter");
66+
act.Should().Throw<ArgumentOutOfRangeException>()
67+
.WithParameterName("parameter");
68+
}
69+
70+
[Fact]
71+
public static void DefaultTolerance_Float()
72+
{
73+
// Should pass - difference is 0.00005f which is less than default tolerance 0.0001f
74+
const float value = 1.00005f;
75+
value.MustBeApproximately(1.0f).Should().Be(value);
76+
77+
// Should throw - difference is 0.0002f which is greater than default tolerance 0.0001f
78+
Action act = () => 1.0002f.MustBeApproximately(1.0f, "parameter");
79+
act.Should().Throw<ArgumentOutOfRangeException>()
80+
.WithParameterName("parameter");
81+
}
82+
83+
[Fact]
84+
public static void CustomException_Double() =>
85+
Test.CustomException(
86+
5.0,
87+
5.3,
88+
(x, y, exceptionFactory) => x.MustBeApproximately(y, exceptionFactory)
89+
);
90+
91+
[Fact]
92+
public static void CustomExceptionWithTolerance_Double() =>
93+
Test.CustomException(
94+
5.0,
95+
5.5,
96+
0.1,
97+
(x, y, z, exceptionFactory) => x.MustBeApproximately(y, z, exceptionFactory)
98+
);
99+
100+
[Fact]
101+
public static void CustomException_Float() =>
102+
Test.CustomException(
103+
5.0f,
104+
5.3f,
105+
(x, y, exceptionFactory) => x.MustBeApproximately(y, exceptionFactory)
106+
);
107+
108+
[Fact]
109+
public static void CustomExceptionWithTolerance_Float() =>
110+
Test.CustomException(
111+
5.0f,
112+
5.5f,
113+
0.1f,
114+
(x, y, z, exceptionFactory) => x.MustBeApproximately(y, z, exceptionFactory)
115+
);
116+
117+
[Fact]
118+
public static void NoCustomExceptionThrown_Double() =>
119+
5.0.MustBeApproximately(5.05, 0.1, (_, _, _) => null).Should().Be(5.0);
120+
121+
[Fact]
122+
public static void NoCustomExceptionThrown_Float() =>
123+
5.0f.MustBeApproximately(5.05f, 0.1f, (_, _, _) => null).Should().Be(5.0f);
124+
125+
[Fact]
126+
public static void CustomMessage_Double() =>
127+
Test.CustomMessage<ArgumentOutOfRangeException>(
128+
message => 100.0.MustBeApproximately(101.0, 0.5, message: message)
129+
);
130+
131+
[Fact]
132+
public static void CustomMessage_Float() =>
133+
Test.CustomMessage<ArgumentOutOfRangeException>(
134+
message => 100.0f.MustBeApproximately(101.0f, 0.5f, message: message)
135+
);
136+
137+
[Fact]
138+
public static void CallerArgumentExpression_Double()
139+
{
140+
const double seventyEightO1 = 78.1;
141+
142+
var act = () => seventyEightO1.MustBeApproximately(3.0);
143+
144+
act.Should().Throw<ArgumentOutOfRangeException>()
145+
.WithParameterName(nameof(seventyEightO1));
146+
}
147+
148+
[Fact]
149+
public static void CallerArgumentExpressionWithTolerance_Double()
150+
{
151+
const double pi = 3.14159;
152+
153+
var act = () => pi.MustBeApproximately(3.0, 0.1);
154+
155+
act.Should().Throw<ArgumentOutOfRangeException>()
156+
.WithParameterName(nameof(pi));
157+
}
158+
159+
[Fact]
160+
public static void CallerArgumentExpression_Float()
161+
{
162+
const float seventyEightO1 = 78.1f;
163+
164+
var act = () => seventyEightO1.MustBeApproximately(3.0f);
165+
166+
act.Should().Throw<ArgumentOutOfRangeException>()
167+
.WithParameterName(nameof(seventyEightO1));
168+
}
169+
170+
[Fact]
171+
public static void CallerArgumentExpressionWithTolerance_Float()
172+
{
173+
const float pi = 3.14159f;
174+
175+
var act = () => pi.MustBeApproximately(3.0f, 0.1f);
176+
177+
act.Should().Throw<ArgumentOutOfRangeException>()
178+
.WithParameterName(nameof(pi));
179+
}
180+
181+
#if NET8_0
182+
[Theory]
183+
[InlineData(5.1, 5.0, 0.2)]
184+
[InlineData(10.3, 10.3, 0.01)]
185+
[InlineData(3.14159, 3.14, 0.002)]
186+
[InlineData(-42.0, -42.0001, 0.001)]
187+
public static void ValuesApproximatelyEqual_Generic(double value, double other, double tolerance) =>
188+
value.MustBeApproximately<double>(other, tolerance).Should().Be(value);
189+
190+
[Theory]
191+
[InlineData(5.0, 5.3, 0.1)]
192+
[InlineData(100.0, 99.8, 0.1)]
193+
[InlineData(-20.0, -20.2, 0.1)]
194+
[InlineData(0.0001, 0.0002, 0.00005)]
195+
public static void ValuesNotApproximatelyEqual_Generic(double value, double other, double tolerance)
196+
{
197+
var act = () => value.MustBeApproximately<double>(other, tolerance, nameof(value));
198+
199+
var exceptionAssertion = act.Should().Throw<ArgumentOutOfRangeException>().Which;
200+
exceptionAssertion.Message.Should().Contain(
201+
$"{nameof(value)} must be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
202+
);
203+
exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
204+
}
205+
206+
[Fact]
207+
public static void CustomExceptionWithTolerance_Generic() =>
208+
Test.CustomException(
209+
5.0,
210+
5.3,
211+
0.2,
212+
(x, y, t, exceptionFactory) => x.MustBeApproximately<double>(y, t, exceptionFactory)
213+
);
214+
215+
[Fact]
216+
public static void NoCustomExceptionThrown_Generic() =>
217+
5.0.MustBeApproximately<double>(5.05, 0.1, (_, _, _) => null).Should().Be(5.0);
218+
219+
[Fact]
220+
public static void CustomMessage_Generic() =>
221+
Test.CustomMessage<ArgumentOutOfRangeException>(
222+
message => 100.0.MustBeApproximately<double>(101.0, 0.5, message: message)
223+
);
224+
225+
[Fact]
226+
public static void CallerArgumentExpressionWithTolerance_Generic()
227+
{
228+
const double seventyEightO1 = 78.1;
229+
230+
var act = () => seventyEightO1.MustBeApproximately<double>(3.0, 10.0);
231+
232+
act.Should().Throw<ArgumentOutOfRangeException>()
233+
.WithParameterName(nameof(seventyEightO1));
234+
}
235+
#endif
236+
}

0 commit comments

Comments
 (0)