Skip to content

Commit ce42aa8

Browse files
authored
✨ Add Equals() to IQuantity interfaces (#1215)
Ref #1193 In v5, the default equality implementation changed to strict equality and the existing methods to compare across units with a tolerance, but this was not available in `IQuantity` interfaces. ### Changes - Add `Equals(IQuantity? other, IQuantity tolerance)` to `IQuantity` - Add `Equals(TSelf? other, TSelf tolerance)` to `IQuantity<TSelf, TUnitType, TValueType>` for strongly typed comparisons - Obsolete `Equals(TQuantity other, double tolerance, ComparisonType comparisonType)` method in quantity types
1 parent 5ecf8e9 commit ce42aa8

File tree

126 files changed

+3753
-1458
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+3753
-1458
lines changed

CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs

+39-21
Original file line numberDiff line numberDiff line change
@@ -330,14 +330,14 @@ private void GenerateStaticMethods()
330330
/// <param name=""unitConverter"">The <see cref=""UnitConverter""/> to register the default conversion functions in.</param>
331331
internal static void RegisterDefaultConversions(UnitConverter unitConverter)
332332
{{
333-
// Register in unit converter: {_quantity.Name}Unit -> BaseUnit");
333+
// Register in unit converter: {_unitEnumName} -> BaseUnit");
334334

335335
foreach (Unit unit in _quantity.Units)
336336
{
337337
if (unit.SingularName == _quantity.BaseUnit) continue;
338338

339339
Writer.WL($@"
340-
unitConverter.SetConversionFunction<{_quantity.Name}>({_quantity.Name}Unit.{unit.SingularName}, {_unitEnumName}.{_quantity.BaseUnit}, quantity => quantity.ToUnit({_unitEnumName}.{_quantity.BaseUnit}));");
340+
unitConverter.SetConversionFunction<{_quantity.Name}>({_unitEnumName}.{unit.SingularName}, {_unitEnumName}.{_quantity.BaseUnit}, quantity => quantity.ToUnit({_unitEnumName}.{_quantity.BaseUnit}));");
341341
}
342342

343343
Writer.WL();
@@ -346,14 +346,14 @@ internal static void RegisterDefaultConversions(UnitConverter unitConverter)
346346
// Register in unit converter: BaseUnit <-> BaseUnit
347347
unitConverter.SetConversionFunction<{_quantity.Name}>({_unitEnumName}.{_quantity.BaseUnit}, {_unitEnumName}.{_quantity.BaseUnit}, quantity => quantity);
348348
349-
// Register in unit converter: BaseUnit -> {_quantity.Name}Unit");
349+
// Register in unit converter: BaseUnit -> {_unitEnumName}");
350350

351351
foreach (Unit unit in _quantity.Units)
352352
{
353353
if (unit.SingularName == _quantity.BaseUnit) continue;
354354

355355
Writer.WL($@"
356-
unitConverter.SetConversionFunction<{_quantity.Name}>({_unitEnumName}.{_quantity.BaseUnit}, {_quantity.Name}Unit.{unit.SingularName}, quantity => quantity.ToUnit({_quantity.Name}Unit.{unit.SingularName}));");
356+
unitConverter.SetConversionFunction<{_quantity.Name}>({_unitEnumName}.{_quantity.BaseUnit}, {_unitEnumName}.{unit.SingularName}, quantity => quantity.ToUnit({_unitEnumName}.{unit.SingularName}));");
357357
}
358358

359359
Writer.WL($@"
@@ -749,25 +749,22 @@ private void GenerateEqualityAndComparison()
749749
#pragma warning disable CS0809
750750
751751
/// <summary>Indicates strict equality of two <see cref=""{_quantity.Name}""/> quantities, where both <see cref=""Value"" /> and <see cref=""Unit"" /> are exactly equal.</summary>
752-
/// <remarks>Consider using <see cref=""Equals({_quantity.Name}, {_valueType}, ComparisonType)""/> to check equality across different units and to specify a floating-point number error tolerance.</remarks>
753-
[Obsolete(""For null checks, use `x is null` syntax to not invoke overloads. For quantity comparisons, use Equals({_quantity.Name}, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
752+
[Obsolete(""For null checks, use `x is null` syntax to not invoke overloads. For equality checks, use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")]
754753
public static bool operator ==({_quantity.Name} left, {_quantity.Name} right)
755754
{{
756755
return left.Equals(right);
757756
}}
758757
759758
/// <summary>Indicates strict inequality of two <see cref=""{_quantity.Name}""/> quantities, where both <see cref=""Value"" /> and <see cref=""Unit"" /> are exactly equal.</summary>
760-
/// <remarks>Consider using <see cref=""Equals({_quantity.Name}, {_valueType}, ComparisonType)""/> to check equality across different units and to specify a floating-point number error tolerance.</remarks>
761-
[Obsolete(""For null checks, use `x is not null` syntax to not invoke overloads. For quantity comparisons, use Equals({_quantity.Name}, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
759+
[Obsolete(""For null checks, use `x is null` syntax to not invoke overloads. For equality checks, use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")]
762760
public static bool operator !=({_quantity.Name} left, {_quantity.Name} right)
763761
{{
764762
return !(left == right);
765763
}}
766764
767765
/// <inheritdoc />
768766
/// <summary>Indicates strict equality of two <see cref=""{_quantity.Name}""/> quantities, where both <see cref=""Value"" /> and <see cref=""Unit"" /> are exactly equal.</summary>
769-
/// <remarks>Consider using <see cref=""Equals({_quantity.Name}, {_valueType}, ComparisonType)""/> to check equality across different units and to specify a floating-point number error tolerance.</remarks>
770-
[Obsolete(""Consider using Equals({_quantity.Name}, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
767+
[Obsolete(""Use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")]
771768
public override bool Equals(object? obj)
772769
{{
773770
if (obj is null || !(obj is {_quantity.Name} otherQuantity))
@@ -778,8 +775,7 @@ public override bool Equals(object? obj)
778775
779776
/// <inheritdoc />
780777
/// <summary>Indicates strict equality of two <see cref=""{_quantity.Name}""/> quantities, where both <see cref=""Value"" /> and <see cref=""Unit"" /> are exactly equal.</summary>
781-
/// <remarks>Consider using <see cref=""Equals({_quantity.Name}, {_valueType}, ComparisonType)""/> to check equality across different units and to specify a floating-point number error tolerance.</remarks>
782-
[Obsolete(""Consider using Equals({_quantity.Name}, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
778+
[Obsolete(""Use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")]
783779
public bool Equals({_quantity.Name} other)
784780
{{
785781
return new {{ Value, Unit }}.Equals(new {{ other.Value, other.Unit }});
@@ -863,15 +859,37 @@ public int CompareTo({_quantity.Name} other)
863859
/// <param name=""tolerance"">The absolute or relative tolerance value. Must be greater than or equal to 0.</param>
864860
/// <param name=""comparisonType"">The comparison type: either relative or absolute.</param>
865861
/// <returns>True if the absolute difference between the two values is not greater than the specified relative or absolute tolerance.</returns>
862+
[Obsolete(""Use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")]
866863
public bool Equals({_quantity.Name} other, {_quantity.ValueType} tolerance, ComparisonType comparisonType)
867864
{{
868865
if (tolerance < 0)
869-
throw new ArgumentOutOfRangeException(""tolerance"", ""Tolerance must be greater than or equal to 0."");
866+
throw new ArgumentOutOfRangeException(nameof(tolerance), ""Tolerance must be greater than or equal to 0."");
870867
871-
{_quantity.ValueType} thisValue = this.Value;
872-
{_quantity.ValueType} otherValueInThisUnits = other.As(this.Unit);
868+
return UnitsNet.Comparison.Equals(
869+
referenceValue: this.Value,
870+
otherValue: other.As(this.Unit),
871+
tolerance: tolerance,
872+
comparisonType: ComparisonType.Absolute);
873+
}}
873874
874-
return UnitsNet.Comparison.Equals(thisValue, otherValueInThisUnits, tolerance, comparisonType);
875+
/// <inheritdoc />
876+
public bool Equals(IQuantity? other, IQuantity tolerance)
877+
{{
878+
return other is {_quantity.Name} otherTyped
879+
&& (tolerance is {_quantity.Name} toleranceTyped
880+
? true
881+
: throw new ArgumentException($""Tolerance quantity ({{tolerance.QuantityInfo.Name}}) did not match the other quantities of type '{_quantity.Name}'."", nameof(tolerance)))
882+
&& Equals(otherTyped, toleranceTyped);
883+
}}
884+
885+
/// <inheritdoc />
886+
public bool Equals({_quantity.Name} other, {_quantity.Name} tolerance)
887+
{{
888+
return UnitsNet.Comparison.Equals(
889+
referenceValue: this.Value,
890+
otherValue: other.As(this.Unit),
891+
tolerance: tolerance.As(this.Unit),
892+
comparisonType: ComparisonType.Absolute);
875893
}}
876894
877895
/// <summary>
@@ -1011,7 +1029,7 @@ double IQuantity.As(Enum unit)
10111029
/// <param name=""unit"">The unit to convert to.</param>
10121030
/// <param name=""converted"">The converted <see cref=""{_quantity.Name}""/> in <paramref name=""unit""/>, if successful.</param>
10131031
/// <returns>True if successful, otherwise false.</returns>
1014-
private bool TryToUnit({_quantity.Name}Unit unit, [NotNullWhen(true)] out {_quantity.Name}? converted)
1032+
private bool TryToUnit({_unitEnumName} unit, [NotNullWhen(true)] out {_quantity.Name}? converted)
10151033
{{
10161034
if (Unit == unit)
10171035
{{
@@ -1021,28 +1039,28 @@ private bool TryToUnit({_quantity.Name}Unit unit, [NotNullWhen(true)] out {_quan
10211039
10221040
{_quantity.Name}? convertedOrNull = (Unit, unit) switch
10231041
{{
1024-
// {_quantity.Name}Unit -> BaseUnit");
1042+
// {_unitEnumName} -> BaseUnit");
10251043

10261044
foreach (Unit unit in _quantity.Units)
10271045
{
10281046
if (unit.SingularName == _quantity.BaseUnit) continue;
10291047

10301048
var func = unit.FromUnitToBaseFunc.Replace("{x}", "_value");
10311049
Writer.WL($@"
1032-
({_quantity.Name}Unit.{unit.SingularName}, {_unitEnumName}.{_quantity.BaseUnit}) => new {_quantity.Name}({func}, {_unitEnumName}.{_quantity.BaseUnit}),");
1050+
({_unitEnumName}.{unit.SingularName}, {_unitEnumName}.{_quantity.BaseUnit}) => new {_quantity.Name}({func}, {_unitEnumName}.{_quantity.BaseUnit}),");
10331051
}
10341052

10351053
Writer.WL();
10361054
Writer.WL($@"
10371055
1038-
// BaseUnit -> {_quantity.Name}Unit");
1056+
// BaseUnit -> {_unitEnumName}");
10391057
foreach(Unit unit in _quantity.Units)
10401058
{
10411059
if (unit.SingularName == _quantity.BaseUnit) continue;
10421060

10431061
var func = unit.FromBaseToUnitFunc.Replace("{x}", "_value");
10441062
Writer.WL($@"
1045-
({_unitEnumName}.{_quantity.BaseUnit}, {_quantity.Name}Unit.{unit.SingularName}) => new {_quantity.Name}({func}, {_quantity.Name}Unit.{unit.SingularName}),");
1063+
({_unitEnumName}.{_quantity.BaseUnit}, {_unitEnumName}.{unit.SingularName}) => new {_quantity.Name}({func}, {_unitEnumName}.{unit.SingularName}),");
10461064
}
10471065

10481066
Writer.WL();

UnitsNet.Tests/CustomQuantities/HowMuch.cs

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public HowMuch(double value, HowMuchUnit unit)
1414
Value = value;
1515
}
1616

17+
public bool Equals(IQuantity? other, IQuantity tolerance) => throw new NotImplementedException();
18+
1719
Enum IQuantity.Unit => Unit;
1820
public HowMuchUnit Unit { get; }
1921

UnitsNet.Tests/DummyIQuantity.cs

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ internal class DummyIQuantity : IQuantity
99

1010
public QuantityInfo QuantityInfo => throw new NotImplementedException();
1111

12+
bool IQuantity.Equals(IQuantity? other, IQuantity tolerance) => throw new NotImplementedException();
13+
1214
public Enum Unit => throw new NotImplementedException();
1315

1416
public QuantityValue Value => throw new NotImplementedException();
@@ -17,6 +19,8 @@ internal class DummyIQuantity : IQuantity
1719

1820
public double As(UnitSystem unitSystem ) => throw new NotImplementedException();
1921

22+
public bool Equals(IQuantity? other, double tolerance, ComparisonType comparisonType) => throw new NotImplementedException();
23+
2024
public string ToString(IFormatProvider? provider) => throw new NotImplementedException();
2125

2226
public string ToString(string? format, IFormatProvider? formatProvider) => throw new NotImplementedException();

UnitsNet.Tests/QuantityTests.cs

+81
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed under MIT No Attribution, see LICENSE file at the root.
22
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.
33

4+
using System;
5+
using System.Globalization;
46
using UnitsNet.Units;
57
using Xunit;
68

@@ -16,5 +18,84 @@ public void GetHashCodeForDifferentQuantitiesWithSameValuesAreNotEqual()
1618

1719
Assert.NotEqual(length.GetHashCode(), area.GetHashCode());
1820
}
21+
22+
[Theory]
23+
[InlineData("10 m", "9.89 m" , "0.1 m", false)] // +/- 0.1m absolute tolerance and some additional margin tolerate rounding errors in test case.
24+
[InlineData("10 m", "9.91 m" , "0.1 m", true)]
25+
[InlineData("10 m", "10.09 m", "0.1 m", true)]
26+
[InlineData("10 m", "1009 cm", "0.1 m", true)] // Different unit, still equal.
27+
[InlineData("10 m", "10.11 m", "0.1 m", false)]
28+
[InlineData("10 m", "8.9 m" , "0.1 m", false)] // +/- 1m relative tolerance (10%) and some additional margin tolerate rounding errors in test case.
29+
public void Equals_IGenericEquatableQuantity(string q1String, string q2String, string toleranceString, bool expectedEqual)
30+
{
31+
// This interfaces implements .NET generic math interfaces.
32+
IQuantity<Length, LengthUnit, double> q1 = ParseLength(q1String);
33+
IQuantity<Length, LengthUnit, double> q2 = ParseLength(q2String);
34+
IQuantity<Length, LengthUnit, double> tolerance = ParseLength(toleranceString);
35+
36+
Assert.Equal(expectedEqual, q1.Equals(q2, tolerance));
37+
}
38+
39+
[Theory]
40+
[InlineData("10 m", "9.89 m" , "0.1 m", false)] // +/- 0.1m absolute tolerance and some additional margin tolerate rounding errors in test case.
41+
[InlineData("10 m", "9.91 m" , "0.1 m", true)]
42+
[InlineData("10 m", "10.09 m", "0.1 m", true)]
43+
[InlineData("10 m", "1009 cm", "0.1 m", true)] // Different unit, still equal.
44+
[InlineData("10 m", "10.11 m", "0.1 m", false)]
45+
[InlineData("10 m", "8.9 m" , "0.1 m", false)] // +/- 1m relative tolerance (10%) and some additional margin tolerate rounding errors in test case.
46+
public void Equals_IQuantity(string q1String, string q2String, string toleranceString, bool expectedEqual)
47+
{
48+
IQuantity q1 = ParseLength(q1String);
49+
IQuantity q2 = ParseLength(q2String);
50+
IQuantity tolerance = ParseLength(toleranceString);
51+
52+
Assert.NotEqual(q1, q2); // Strict equality should not be equal.
53+
Assert.Equal(expectedEqual, q1.Equals(q2, tolerance));
54+
}
55+
56+
[Fact]
57+
public void Equals_IQuantity_OtherIsNull_ReturnsFalse()
58+
{
59+
IQuantity q1 = ParseLength("10 m");
60+
IQuantity? q2 = null;
61+
IQuantity tolerance = ParseLength("0.1 m");
62+
63+
Assert.False(q1.Equals(q2, tolerance));
64+
}
65+
66+
[Fact]
67+
public void Equals_IQuantity_OtherIsDifferentType_ReturnsFalse()
68+
{
69+
IQuantity q1 = ParseLength("10 m");
70+
IQuantity q2 = Mass.FromKilograms(10);
71+
IQuantity tolerance = Mass.FromKilograms(0.1);
72+
73+
Assert.False(q1.Equals(q2, tolerance));
74+
}
75+
76+
[Fact]
77+
public void Equals_IQuantity_ToleranceIsDifferentType_Throws()
78+
{
79+
IQuantity q1 = ParseLength("10 m");
80+
IQuantity q2 = ParseLength("10 m");
81+
IQuantity tolerance = Mass.FromKilograms(0.1);
82+
83+
Assert.Throws<ArgumentException>(() => q1.Equals(q2, tolerance));
84+
}
85+
86+
[Fact]
87+
public void Equals_GenericEquatableIQuantity_OtherIsNull_ReturnsFalse()
88+
{
89+
IQuantity<Length, LengthUnit, double> q1 = ParseLength("10 m");
90+
IQuantity<Length, LengthUnit, double>? q2 = null;
91+
IQuantity<Length, LengthUnit, double> tolerance = ParseLength("0.1 m");
92+
93+
Assert.False(q1.Equals(q2, tolerance));
94+
}
95+
96+
private static Length ParseLength(string str)
97+
{
98+
return Length.Parse(str, CultureInfo.InvariantCulture);
99+
}
19100
}
20101
}

UnitsNet.Tests/UnitsNet.Tests.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<TargetFrameworks>net7.0;net48</TargetFrameworks>
55
<LangVersion>latest</LangVersion>
66
<IsTestProject>true</IsTestProject>
7-
<NoWarn>CS0618</NoWarn>
7+
<NoWarn>CS0618</NoWarn><!-- CS0618: 'member' is obsolete: 'text' (we often obsolete things before removal) -->
88
<Nullable>enable</Nullable>
99
</PropertyGroup>
1010

0 commit comments

Comments
 (0)