Skip to content

Commit d10afc9

Browse files
authored
✨ Add back IEquatable with strict equality (#1100)
Fixes #1017 - Reverted removing `IEquatable`. - Changed the implementation to strict equality on both `Value` and `Unit`. - Improved tests. - Improved xmldocs for equality members. - `GetHashCode()` left unchanged, which includes quantity name in addition to value and unit, so that `LengthUnit.Meter = 1` and `MassUnit.Gram = 1` are still considered different in collections of `IQuantity`. Reverts commit f3c7e25. "🔥 Remove IEquatable<T> and equality operators/methods"
1 parent ed3da18 commit d10afc9

File tree

241 files changed

+13111
-711
lines changed

Some content is hidden

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

241 files changed

+13111
-711
lines changed

CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs

+70-6
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ namespace UnitsNet
7777
Writer.W("IDecimalQuantity, ");
7878
}
7979

80-
Writer.WL($"IComparable, IComparable<{_quantity.Name}>, IConvertible, IFormattable");
80+
Writer.WL($"IEquatable<{_quantity.Name}>, IComparable, IComparable<{_quantity.Name}>, IConvertible, IFormattable");
8181
Writer.WL($@"
8282
{{
8383
/// <summary>
@@ -752,16 +752,80 @@ private void GenerateEqualityAndComparison()
752752
return left.Value > right.ToUnit(left.Unit).Value;
753753
}}
754754
755+
// We use obsolete attribute to communicate the preferred equality members to use.
756+
// CS0809: Obsolete member 'memberA' overrides non-obsolete member 'memberB'.
757+
#pragma warning disable CS0809
758+
759+
/// <summary>Indicates strict equality 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(""Consider using Equals(Angle, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
762+
public static bool operator ==({_quantity.Name} left, {_quantity.Name} right)
763+
{{
764+
return left.Equals(right);
765+
}}
766+
767+
/// <summary>Indicates strict inequality of two <see cref=""{_quantity.Name}""/> quantities, where both <see cref=""Value"" /> and <see cref=""Unit"" /> are exactly equal.</summary>
768+
/// <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>
769+
[Obsolete(""Consider using Equals(Angle, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
770+
public static bool operator !=({_quantity.Name} left, {_quantity.Name} right)
771+
{{
772+
return !(left == right);
773+
}}
774+
775+
/// <inheritdoc />
776+
/// <summary>Indicates strict equality of two <see cref=""{_quantity.Name}""/> quantities, where both <see cref=""Value"" /> and <see cref=""Unit"" /> are exactly equal.</summary>
777+
/// <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>
778+
[Obsolete(""Consider using Equals(Angle, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
779+
public override bool Equals(object obj)
780+
{{
781+
if (obj is null || !(obj is {_quantity.Name} otherQuantity))
782+
return false;
783+
784+
return Equals(otherQuantity);
785+
}}
786+
755787
/// <inheritdoc />
788+
/// <summary>Indicates strict equality of two <see cref=""{_quantity.Name}""/> quantities, where both <see cref=""Value"" /> and <see cref=""Unit"" /> are exactly equal.</summary>
789+
/// <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>
790+
[Obsolete(""Consider using Equals(Angle, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
791+
public bool Equals({_quantity.Name} other)
792+
{{
793+
return new {{ Value, Unit }}.Equals(new {{ other.Value, other.Unit }});
794+
}}
795+
796+
#pragma warning restore CS0809
797+
798+
/// <summary>Compares the current <see cref=""{_quantity.Name}""/> with another object of the same type and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other when converted to the same unit.</summary>
799+
/// <param name=""obj"">An object to compare with this instance.</param>
800+
/// <exception cref=""T:System.ArgumentException"">
801+
/// <paramref name=""obj"" /> is not the same type as this instance.
802+
/// </exception>
803+
/// <returns>A value that indicates the relative order of the quantities being compared. The return value has these meanings:
804+
/// <list type=""table"">
805+
/// <listheader><term> Value</term><description> Meaning</description></listheader>
806+
/// <item><term> Less than zero</term><description> This instance precedes <paramref name=""obj"" /> in the sort order.</description></item>
807+
/// <item><term> Zero</term><description> This instance occurs in the same position in the sort order as <paramref name=""obj"" />.</description></item>
808+
/// <item><term> Greater than zero</term><description> This instance follows <paramref name=""obj"" /> in the sort order.</description></item>
809+
/// </list>
810+
/// </returns>
756811
public int CompareTo(object obj)
757812
{{
758813
if (obj is null) throw new ArgumentNullException(nameof(obj));
759-
if (!(obj is {_quantity.Name} obj{_quantity.Name})) throw new ArgumentException(""Expected type {_quantity.Name}."", nameof(obj));
814+
if (!(obj is {_quantity.Name} otherQuantity)) throw new ArgumentException(""Expected type {_quantity.Name}."", nameof(obj));
760815
761-
return CompareTo(obj{_quantity.Name});
816+
return CompareTo(otherQuantity);
762817
}}
763818
764-
/// <inheritdoc />
819+
/// <summary>Compares the current <see cref=""{_quantity.Name}""/> with another <see cref=""{_quantity.Name}""/> and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other when converted to the same unit.</summary>
820+
/// <param name=""other"">A quantity to compare with this instance.</param>
821+
/// <returns>A value that indicates the relative order of the quantities being compared. The return value has these meanings:
822+
/// <list type=""table"">
823+
/// <listheader><term> Value</term><description> Meaning</description></listheader>
824+
/// <item><term> Less than zero</term><description> This instance precedes <paramref name=""other"" /> in the sort order.</description></item>
825+
/// <item><term> Zero</term><description> This instance occurs in the same position in the sort order as <paramref name=""other"" />.</description></item>
826+
/// <item><term> Greater than zero</term><description> This instance follows <paramref name=""other"" /> in the sort order.</description></item>
827+
/// </list>
828+
/// </returns>
765829
public int CompareTo({_quantity.Name} other)
766830
{{
767831
return _value.CompareTo(other.ToUnit(this.Unit).Value);
@@ -800,7 +864,7 @@ public int CompareTo({_quantity.Name} other)
800864
/// </para>
801865
/// <para>
802866
/// Note that it is advised against specifying zero difference, due to the nature
803-
/// of floating point operations and using System.Double internally.
867+
/// of floating-point operations and using {_valueType} internally.
804868
/// </para>
805869
/// </summary>
806870
/// <param name=""other"">The other quantity to compare to.</param>
@@ -987,7 +1051,7 @@ private bool TryToUnit({_quantity.Name}Unit unit, out {_quantity.Name}? converte
9871051
_ => null!
9881052
}};
9891053
990-
return converted != null;
1054+
return converted is not null;
9911055
}}
9921056
9931057
/// <inheritdoc />

CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs

+62
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,32 @@ internal class UnitTestBaseClassGenerator : GeneratorBase
4545
/// </summary>
4646
private readonly string _numberSuffix;
4747

48+
/// <summary>
49+
/// Other unit, if more than one unit exists for quantity, otherwise same as <see cref="_baseUnit"/>.
50+
/// </summary>
51+
private readonly Unit _otherOrBaseUnit;
52+
53+
/// <summary>
54+
/// Example: "LengthUnit.Centimeter".
55+
/// </summary>
56+
private readonly string _otherOrBaseUnitFullName;
57+
4858
public UnitTestBaseClassGenerator(Quantity quantity)
4959
{
5060
_quantity = quantity;
5161
_baseUnit = quantity.Units.FirstOrDefault(u => u.SingularName == _quantity.BaseUnit) ??
5262
throw new ArgumentException($"No unit found with SingularName equal to BaseUnit [{_quantity.BaseUnit}]. This unit must be defined.",
5363
nameof(quantity));
64+
5465
_unitEnumName = $"{quantity.Name}Unit";
66+
5567
_baseUnitEnglishAbbreviation = GetEnglishAbbreviation(_baseUnit);
5668
_baseUnitFullName = $"{_unitEnumName}.{_baseUnit.SingularName}";
5769
_numberSuffix = quantity.ValueType == "decimal" ? "m" : "";
70+
71+
// Try to pick another unit, or fall back to base unit if only a single unit.
72+
_otherOrBaseUnit = quantity.Units.Where(u => u != _baseUnit).DefaultIfEmpty(_baseUnit).First();
73+
_otherOrBaseUnitFullName = $"{_unitEnumName}.{_otherOrBaseUnit.SingularName}";
5874
}
5975

6076
private string GetUnitFullName(Unit unit) => $"{_unitEnumName}.{unit.SingularName}";
@@ -483,6 +499,52 @@ public void CompareToThrowsOnNull()
483499
Assert.Throws<ArgumentNullException>(() => {baseUnitVariableName}.CompareTo(null));
484500
}}
485501
502+
[Theory]
503+
[InlineData(1, {_baseUnitFullName}, 1, {_baseUnitFullName}, true)] // Same value and unit.
504+
[InlineData(1, {_baseUnitFullName}, 2, {_baseUnitFullName}, false)] // Different value.
505+
[InlineData(2, {_baseUnitFullName}, 1, {_otherOrBaseUnitFullName}, false)] // Different value and unit.");
506+
if (_baseUnit != _otherOrBaseUnit)
507+
{
508+
Writer.WL($@"
509+
[InlineData(1, {_baseUnitFullName}, 1, {_otherOrBaseUnitFullName}, false)] // Different unit.");
510+
}
511+
Writer.WL($@"
512+
public void Equals_ReturnsTrue_IfValueAndUnitAreEqual({_quantity.ValueType} valueA, {_unitEnumName} unitA, {_quantity.ValueType} valueB, {_unitEnumName} unitB, bool expectEqual)
513+
{{
514+
var a = new {_quantity.Name}(valueA, unitA);
515+
var b = new {_quantity.Name}(valueB, unitB);
516+
517+
// Operator overloads.
518+
Assert.Equal(expectEqual, a == b);
519+
Assert.Equal(expectEqual, b == a);
520+
Assert.Equal(!expectEqual, a != b);
521+
Assert.Equal(!expectEqual, b != a);
522+
523+
// IEquatable<T>
524+
Assert.Equal(expectEqual, a.Equals(b));
525+
Assert.Equal(expectEqual, b.Equals(a));
526+
527+
// IEquatable
528+
Assert.Equal(expectEqual, a.Equals((object)b));
529+
Assert.Equal(expectEqual, b.Equals((object)a));
530+
}}
531+
532+
[Fact]
533+
public void Equals_Null_ReturnsFalse()
534+
{{
535+
var a = {_quantity.Name}.Zero;
536+
537+
Assert.False(a.Equals((object)null));
538+
539+
// ""The result of the expression is always 'false'...""
540+
#pragma warning disable CS8073
541+
Assert.False(a == null);
542+
Assert.False(null == a);
543+
Assert.True(a != null);
544+
Assert.True(null != a);
545+
#pragma warning restore CS8073
546+
}}
547+
486548
[Fact]
487549
public void Equals_RelativeTolerance_IsImplemented()
488550
{{

UnitsNet.NumberExtensions.Tests/UnitsNet.NumberExtensions.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<LangVersion>latest</LangVersion>
77
<Nullable>enable</Nullable>
88
<IsTestProject>true</IsTestProject>
9+
<NoWarn>CS0618</NoWarn>
910
</PropertyGroup>
1011

1112
<!-- Strong name signing -->

UnitsNet.Serialization.JsonNet.Tests/UnitsNet.Serialization.JsonNet.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<RootNamespace>UnitsNet.Serialization.JsonNet.Tests</RootNamespace>
66
<LangVersion>latest</LangVersion>
77
<IsTestProject>true</IsTestProject>
8+
<NoWarn>CS0618</NoWarn>
89
</PropertyGroup>
910

1011
<ItemGroup>

UnitsNet.Tests/CustomCode/AreaDensityTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class AreaDensityTests : AreaDensityTestsBase
4040
public void AreaDensityTimesAreaEqualsMass()
4141
{
4242
Mass massOfOneA4Paper = AreaDensity.FromGramsPerSquareMeter(120) * Area.FromSquareCentimeters(625);
43-
Assert.Equal(Mass.FromGrams(7.5), massOfOneA4Paper);
43+
Assert.Equal(7.5, massOfOneA4Paper.Grams);
4444
}
4545
}
4646
}

UnitsNet.Tests/CustomCode/TemperatureDeltaTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public void TemperatureDeltaTimesSpecificEntropyEqualsSpecificEnergy()
5555
public void EntropyTimesTemperatureDeltaEqualsEnergy()
5656
{
5757
Energy energy = Entropy.FromKilojoulesPerKelvin(3) * TemperatureDelta.FromKelvins(7);
58-
Assert.Equal(Energy.FromKilojoules(21), energy);
58+
Assert.Equal(21, energy.Kilojoules);
5959
}
6060

6161
[Fact]

UnitsNet.Tests/GeneratedCode/TestsBase/AccelerationTestsBase.g.cs

+41
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

UnitsNet.Tests/GeneratedCode/TestsBase/AmountOfSubstanceTestsBase.g.cs

+41
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

UnitsNet.Tests/GeneratedCode/TestsBase/AmplitudeRatioTestsBase.g.cs

+41
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)