Skip to content

Commit

Permalink
✨ Add Equals() to IQuantity interfaces (#1215)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
angularsen authored May 26, 2023
1 parent 5ecf8e9 commit ce42aa8
Show file tree
Hide file tree
Showing 126 changed files with 3,753 additions and 1,458 deletions.
60 changes: 39 additions & 21 deletions CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,14 +330,14 @@ private void GenerateStaticMethods()
/// <param name=""unitConverter"">The <see cref=""UnitConverter""/> to register the default conversion functions in.</param>
internal static void RegisterDefaultConversions(UnitConverter unitConverter)
{{
// Register in unit converter: {_quantity.Name}Unit -> BaseUnit");
// Register in unit converter: {_unitEnumName} -> BaseUnit");

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

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

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

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

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

Writer.WL($@"
Expand Down Expand Up @@ -749,25 +749,22 @@ private void GenerateEqualityAndComparison()
#pragma warning disable CS0809
/// <summary>Indicates strict equality of two <see cref=""{_quantity.Name}""/> quantities, where both <see cref=""Value"" /> and <see cref=""Unit"" /> are exactly equal.</summary>
/// <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>
[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."")]
[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."")]
public static bool operator ==({_quantity.Name} left, {_quantity.Name} right)
{{
return left.Equals(right);
}}
/// <summary>Indicates strict inequality of two <see cref=""{_quantity.Name}""/> quantities, where both <see cref=""Value"" /> and <see cref=""Unit"" /> are exactly equal.</summary>
/// <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>
[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."")]
[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."")]
public static bool operator !=({_quantity.Name} left, {_quantity.Name} right)
{{
return !(left == right);
}}
/// <inheritdoc />
/// <summary>Indicates strict equality of two <see cref=""{_quantity.Name}""/> quantities, where both <see cref=""Value"" /> and <see cref=""Unit"" /> are exactly equal.</summary>
/// <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>
[Obsolete(""Consider using Equals({_quantity.Name}, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
[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."")]
public override bool Equals(object? obj)
{{
if (obj is null || !(obj is {_quantity.Name} otherQuantity))
Expand All @@ -778,8 +775,7 @@ public override bool Equals(object? obj)
/// <inheritdoc />
/// <summary>Indicates strict equality of two <see cref=""{_quantity.Name}""/> quantities, where both <see cref=""Value"" /> and <see cref=""Unit"" /> are exactly equal.</summary>
/// <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>
[Obsolete(""Consider using Equals({_quantity.Name}, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
[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."")]
public bool Equals({_quantity.Name} other)
{{
return new {{ Value, Unit }}.Equals(new {{ other.Value, other.Unit }});
Expand Down Expand Up @@ -863,15 +859,37 @@ public int CompareTo({_quantity.Name} other)
/// <param name=""tolerance"">The absolute or relative tolerance value. Must be greater than or equal to 0.</param>
/// <param name=""comparisonType"">The comparison type: either relative or absolute.</param>
/// <returns>True if the absolute difference between the two values is not greater than the specified relative or absolute tolerance.</returns>
[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."")]
public bool Equals({_quantity.Name} other, {_quantity.ValueType} tolerance, ComparisonType comparisonType)
{{
if (tolerance < 0)
throw new ArgumentOutOfRangeException(""tolerance"", ""Tolerance must be greater than or equal to 0."");
throw new ArgumentOutOfRangeException(nameof(tolerance), ""Tolerance must be greater than or equal to 0."");
{_quantity.ValueType} thisValue = this.Value;
{_quantity.ValueType} otherValueInThisUnits = other.As(this.Unit);
return UnitsNet.Comparison.Equals(
referenceValue: this.Value,
otherValue: other.As(this.Unit),
tolerance: tolerance,
comparisonType: ComparisonType.Absolute);
}}
return UnitsNet.Comparison.Equals(thisValue, otherValueInThisUnits, tolerance, comparisonType);
/// <inheritdoc />
public bool Equals(IQuantity? other, IQuantity tolerance)
{{
return other is {_quantity.Name} otherTyped
&& (tolerance is {_quantity.Name} toleranceTyped
? true
: throw new ArgumentException($""Tolerance quantity ({{tolerance.QuantityInfo.Name}}) did not match the other quantities of type '{_quantity.Name}'."", nameof(tolerance)))
&& Equals(otherTyped, toleranceTyped);
}}
/// <inheritdoc />
public bool Equals({_quantity.Name} other, {_quantity.Name} tolerance)
{{
return UnitsNet.Comparison.Equals(
referenceValue: this.Value,
otherValue: other.As(this.Unit),
tolerance: tolerance.As(this.Unit),
comparisonType: ComparisonType.Absolute);
}}
/// <summary>
Expand Down Expand Up @@ -1011,7 +1029,7 @@ double IQuantity.As(Enum unit)
/// <param name=""unit"">The unit to convert to.</param>
/// <param name=""converted"">The converted <see cref=""{_quantity.Name}""/> in <paramref name=""unit""/>, if successful.</param>
/// <returns>True if successful, otherwise false.</returns>
private bool TryToUnit({_quantity.Name}Unit unit, [NotNullWhen(true)] out {_quantity.Name}? converted)
private bool TryToUnit({_unitEnumName} unit, [NotNullWhen(true)] out {_quantity.Name}? converted)
{{
if (Unit == unit)
{{
Expand All @@ -1021,28 +1039,28 @@ private bool TryToUnit({_quantity.Name}Unit unit, [NotNullWhen(true)] out {_quan
{_quantity.Name}? convertedOrNull = (Unit, unit) switch
{{
// {_quantity.Name}Unit -> BaseUnit");
// {_unitEnumName} -> BaseUnit");

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

var func = unit.FromUnitToBaseFunc.Replace("{x}", "_value");
Writer.WL($@"
({_quantity.Name}Unit.{unit.SingularName}, {_unitEnumName}.{_quantity.BaseUnit}) => new {_quantity.Name}({func}, {_unitEnumName}.{_quantity.BaseUnit}),");
({_unitEnumName}.{unit.SingularName}, {_unitEnumName}.{_quantity.BaseUnit}) => new {_quantity.Name}({func}, {_unitEnumName}.{_quantity.BaseUnit}),");
}

Writer.WL();
Writer.WL($@"
// BaseUnit -> {_quantity.Name}Unit");
// BaseUnit -> {_unitEnumName}");
foreach(Unit unit in _quantity.Units)
{
if (unit.SingularName == _quantity.BaseUnit) continue;

var func = unit.FromBaseToUnitFunc.Replace("{x}", "_value");
Writer.WL($@"
({_unitEnumName}.{_quantity.BaseUnit}, {_quantity.Name}Unit.{unit.SingularName}) => new {_quantity.Name}({func}, {_quantity.Name}Unit.{unit.SingularName}),");
({_unitEnumName}.{_quantity.BaseUnit}, {_unitEnumName}.{unit.SingularName}) => new {_quantity.Name}({func}, {_unitEnumName}.{unit.SingularName}),");
}

Writer.WL();
Expand Down
2 changes: 2 additions & 0 deletions UnitsNet.Tests/CustomQuantities/HowMuch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public HowMuch(double value, HowMuchUnit unit)
Value = value;
}

public bool Equals(IQuantity? other, IQuantity tolerance) => throw new NotImplementedException();

Enum IQuantity.Unit => Unit;
public HowMuchUnit Unit { get; }

Expand Down
4 changes: 4 additions & 0 deletions UnitsNet.Tests/DummyIQuantity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ internal class DummyIQuantity : IQuantity

public QuantityInfo QuantityInfo => throw new NotImplementedException();

bool IQuantity.Equals(IQuantity? other, IQuantity tolerance) => throw new NotImplementedException();

public Enum Unit => throw new NotImplementedException();

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

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

public bool Equals(IQuantity? other, double tolerance, ComparisonType comparisonType) => throw new NotImplementedException();

public string ToString(IFormatProvider? provider) => throw new NotImplementedException();

public string ToString(string? format, IFormatProvider? formatProvider) => throw new NotImplementedException();
Expand Down
81 changes: 81 additions & 0 deletions UnitsNet.Tests/QuantityTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed under MIT No Attribution, see LICENSE file at the root.
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.

using System;
using System.Globalization;
using UnitsNet.Units;
using Xunit;

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

Assert.NotEqual(length.GetHashCode(), area.GetHashCode());
}

[Theory]
[InlineData("10 m", "9.89 m" , "0.1 m", false)] // +/- 0.1m absolute tolerance and some additional margin tolerate rounding errors in test case.
[InlineData("10 m", "9.91 m" , "0.1 m", true)]
[InlineData("10 m", "10.09 m", "0.1 m", true)]
[InlineData("10 m", "1009 cm", "0.1 m", true)] // Different unit, still equal.
[InlineData("10 m", "10.11 m", "0.1 m", false)]
[InlineData("10 m", "8.9 m" , "0.1 m", false)] // +/- 1m relative tolerance (10%) and some additional margin tolerate rounding errors in test case.
public void Equals_IGenericEquatableQuantity(string q1String, string q2String, string toleranceString, bool expectedEqual)
{
// This interfaces implements .NET generic math interfaces.
IQuantity<Length, LengthUnit, double> q1 = ParseLength(q1String);
IQuantity<Length, LengthUnit, double> q2 = ParseLength(q2String);
IQuantity<Length, LengthUnit, double> tolerance = ParseLength(toleranceString);

Assert.Equal(expectedEqual, q1.Equals(q2, tolerance));
}

[Theory]
[InlineData("10 m", "9.89 m" , "0.1 m", false)] // +/- 0.1m absolute tolerance and some additional margin tolerate rounding errors in test case.
[InlineData("10 m", "9.91 m" , "0.1 m", true)]
[InlineData("10 m", "10.09 m", "0.1 m", true)]
[InlineData("10 m", "1009 cm", "0.1 m", true)] // Different unit, still equal.
[InlineData("10 m", "10.11 m", "0.1 m", false)]
[InlineData("10 m", "8.9 m" , "0.1 m", false)] // +/- 1m relative tolerance (10%) and some additional margin tolerate rounding errors in test case.
public void Equals_IQuantity(string q1String, string q2String, string toleranceString, bool expectedEqual)
{
IQuantity q1 = ParseLength(q1String);
IQuantity q2 = ParseLength(q2String);
IQuantity tolerance = ParseLength(toleranceString);

Assert.NotEqual(q1, q2); // Strict equality should not be equal.
Assert.Equal(expectedEqual, q1.Equals(q2, tolerance));
}

[Fact]
public void Equals_IQuantity_OtherIsNull_ReturnsFalse()
{
IQuantity q1 = ParseLength("10 m");
IQuantity? q2 = null;
IQuantity tolerance = ParseLength("0.1 m");

Assert.False(q1.Equals(q2, tolerance));
}

[Fact]
public void Equals_IQuantity_OtherIsDifferentType_ReturnsFalse()
{
IQuantity q1 = ParseLength("10 m");
IQuantity q2 = Mass.FromKilograms(10);
IQuantity tolerance = Mass.FromKilograms(0.1);

Assert.False(q1.Equals(q2, tolerance));
}

[Fact]
public void Equals_IQuantity_ToleranceIsDifferentType_Throws()
{
IQuantity q1 = ParseLength("10 m");
IQuantity q2 = ParseLength("10 m");
IQuantity tolerance = Mass.FromKilograms(0.1);

Assert.Throws<ArgumentException>(() => q1.Equals(q2, tolerance));
}

[Fact]
public void Equals_GenericEquatableIQuantity_OtherIsNull_ReturnsFalse()
{
IQuantity<Length, LengthUnit, double> q1 = ParseLength("10 m");
IQuantity<Length, LengthUnit, double>? q2 = null;
IQuantity<Length, LengthUnit, double> tolerance = ParseLength("0.1 m");

Assert.False(q1.Equals(q2, tolerance));
}

private static Length ParseLength(string str)
{
return Length.Parse(str, CultureInfo.InvariantCulture);
}
}
}
2 changes: 1 addition & 1 deletion UnitsNet.Tests/UnitsNet.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFrameworks>net7.0;net48</TargetFrameworks>
<LangVersion>latest</LangVersion>
<IsTestProject>true</IsTestProject>
<NoWarn>CS0618</NoWarn>
<NoWarn>CS0618</NoWarn><!-- CS0618: 'member' is obsolete: 'text' (we often obsolete things before removal) -->
<Nullable>enable</Nullable>
</PropertyGroup>

Expand Down
Loading

0 comments on commit ce42aa8

Please sign in to comment.