Skip to content

✨ Add back IEquatable with strict equality #1100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 70 additions & 6 deletions CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ namespace UnitsNet
Writer.W("IDecimalQuantity, ");
}

Writer.WL($"IComparable, IComparable<{_quantity.Name}>, IConvertible, IFormattable");
Writer.WL($"IEquatable<{_quantity.Name}>, IComparable, IComparable<{_quantity.Name}>, IConvertible, IFormattable");
Writer.WL($@"
{{
/// <summary>
Expand Down Expand Up @@ -752,16 +752,80 @@ private void GenerateEqualityAndComparison()
return left.Value > right.ToUnit(left.Unit).Value;
}}

// We use obsolete attribute to communicate the preferred equality members to use.
// CS0809: Obsolete member 'memberA' overrides non-obsolete member 'memberB'.
#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(""Consider using Equals(Angle, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
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(""Consider using Equals(Angle, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
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(Angle, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
public override bool Equals(object obj)
{{
if (obj is null || !(obj is {_quantity.Name} otherQuantity))
return false;

return Equals(otherQuantity);
}}

/// <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(Angle, {_valueType}, ComparisonType) to check equality across different units and to specify a floating-point number error tolerance."")]
public bool Equals({_quantity.Name} other)
{{
return new {{ Value, Unit }}.Equals(new {{ other.Value, other.Unit }});
}}

#pragma warning restore CS0809

/// <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>
/// <param name=""obj"">An object to compare with this instance.</param>
/// <exception cref=""T:System.ArgumentException"">
/// <paramref name=""obj"" /> is not the same type as this instance.
/// </exception>
/// <returns>A value that indicates the relative order of the quantities being compared. The return value has these meanings:
/// <list type=""table"">
/// <listheader><term> Value</term><description> Meaning</description></listheader>
/// <item><term> Less than zero</term><description> This instance precedes <paramref name=""obj"" /> in the sort order.</description></item>
/// <item><term> Zero</term><description> This instance occurs in the same position in the sort order as <paramref name=""obj"" />.</description></item>
/// <item><term> Greater than zero</term><description> This instance follows <paramref name=""obj"" /> in the sort order.</description></item>
/// </list>
/// </returns>
public int CompareTo(object obj)
{{
if (obj is null) throw new ArgumentNullException(nameof(obj));
if (!(obj is {_quantity.Name} obj{_quantity.Name})) throw new ArgumentException(""Expected type {_quantity.Name}."", nameof(obj));
if (!(obj is {_quantity.Name} otherQuantity)) throw new ArgumentException(""Expected type {_quantity.Name}."", nameof(obj));

return CompareTo(obj{_quantity.Name});
return CompareTo(otherQuantity);
}}

/// <inheritdoc />
/// <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>
/// <param name=""other"">A quantity to compare with this instance.</param>
/// <returns>A value that indicates the relative order of the quantities being compared. The return value has these meanings:
/// <list type=""table"">
/// <listheader><term> Value</term><description> Meaning</description></listheader>
/// <item><term> Less than zero</term><description> This instance precedes <paramref name=""other"" /> in the sort order.</description></item>
/// <item><term> Zero</term><description> This instance occurs in the same position in the sort order as <paramref name=""other"" />.</description></item>
/// <item><term> Greater than zero</term><description> This instance follows <paramref name=""other"" /> in the sort order.</description></item>
/// </list>
/// </returns>
public int CompareTo({_quantity.Name} other)
{{
return _value.CompareTo(other.ToUnit(this.Unit).Value);
Expand Down Expand Up @@ -800,7 +864,7 @@ public int CompareTo({_quantity.Name} other)
/// </para>
/// <para>
/// Note that it is advised against specifying zero difference, due to the nature
/// of floating point operations and using System.Double internally.
/// of floating-point operations and using {_valueType} internally.
/// </para>
/// </summary>
/// <param name=""other"">The other quantity to compare to.</param>
Expand Down Expand Up @@ -987,7 +1051,7 @@ private bool TryToUnit({_quantity.Name}Unit unit, out {_quantity.Name}? converte
_ => null!
}};

return converted != null;
return converted is not null;
}}

/// <inheritdoc />
Expand Down
62 changes: 62 additions & 0 deletions CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,32 @@ internal class UnitTestBaseClassGenerator : GeneratorBase
/// </summary>
private readonly string _numberSuffix;

/// <summary>
/// Other unit, if more than one unit exists for quantity, otherwise same as <see cref="_baseUnit"/>.
/// </summary>
private readonly Unit _otherOrBaseUnit;

/// <summary>
/// Example: "LengthUnit.Centimeter".
/// </summary>
private readonly string _otherOrBaseUnitFullName;

public UnitTestBaseClassGenerator(Quantity quantity)
{
_quantity = quantity;
_baseUnit = quantity.Units.FirstOrDefault(u => u.SingularName == _quantity.BaseUnit) ??
throw new ArgumentException($"No unit found with SingularName equal to BaseUnit [{_quantity.BaseUnit}]. This unit must be defined.",
nameof(quantity));

_unitEnumName = $"{quantity.Name}Unit";

_baseUnitEnglishAbbreviation = GetEnglishAbbreviation(_baseUnit);
_baseUnitFullName = $"{_unitEnumName}.{_baseUnit.SingularName}";
_numberSuffix = quantity.ValueType == "decimal" ? "m" : "";

// Try to pick another unit, or fall back to base unit if only a single unit.
_otherOrBaseUnit = quantity.Units.Where(u => u != _baseUnit).DefaultIfEmpty(_baseUnit).First();
_otherOrBaseUnitFullName = $"{_unitEnumName}.{_otherOrBaseUnit.SingularName}";
}

private string GetUnitFullName(Unit unit) => $"{_unitEnumName}.{unit.SingularName}";
Expand Down Expand Up @@ -483,6 +499,52 @@ public void CompareToThrowsOnNull()
Assert.Throws<ArgumentNullException>(() => {baseUnitVariableName}.CompareTo(null));
}}

[Theory]
[InlineData(1, {_baseUnitFullName}, 1, {_baseUnitFullName}, true)] // Same value and unit.
[InlineData(1, {_baseUnitFullName}, 2, {_baseUnitFullName}, false)] // Different value.
[InlineData(2, {_baseUnitFullName}, 1, {_otherOrBaseUnitFullName}, false)] // Different value and unit.");
if (_baseUnit != _otherOrBaseUnit)
{
Writer.WL($@"
[InlineData(1, {_baseUnitFullName}, 1, {_otherOrBaseUnitFullName}, false)] // Different unit.");
}
Writer.WL($@"
public void Equals_ReturnsTrue_IfValueAndUnitAreEqual({_quantity.ValueType} valueA, {_unitEnumName} unitA, {_quantity.ValueType} valueB, {_unitEnumName} unitB, bool expectEqual)
{{
var a = new {_quantity.Name}(valueA, unitA);
var b = new {_quantity.Name}(valueB, unitB);

// Operator overloads.
Assert.Equal(expectEqual, a == b);
Assert.Equal(expectEqual, b == a);
Assert.Equal(!expectEqual, a != b);
Assert.Equal(!expectEqual, b != a);

// IEquatable<T>
Assert.Equal(expectEqual, a.Equals(b));
Assert.Equal(expectEqual, b.Equals(a));

// IEquatable
Assert.Equal(expectEqual, a.Equals((object)b));
Assert.Equal(expectEqual, b.Equals((object)a));
}}

[Fact]
public void Equals_Null_ReturnsFalse()
{{
var a = {_quantity.Name}.Zero;

Assert.False(a.Equals((object)null));

// ""The result of the expression is always 'false'...""
#pragma warning disable CS8073
Assert.False(a == null);
Assert.False(null == a);
Assert.True(a != null);
Assert.True(null != a);
#pragma warning restore CS8073
}}

[Fact]
public void Equals_RelativeTolerance_IsImplemented()
{{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IsTestProject>true</IsTestProject>
<NoWarn>CS0618</NoWarn>
</PropertyGroup>

<!-- Strong name signing -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<RootNamespace>UnitsNet.Serialization.JsonNet.Tests</RootNamespace>
<LangVersion>latest</LangVersion>
<IsTestProject>true</IsTestProject>
<NoWarn>CS0618</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion UnitsNet.Tests/CustomCode/AreaDensityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class AreaDensityTests : AreaDensityTestsBase
public void AreaDensityTimesAreaEqualsMass()
{
Mass massOfOneA4Paper = AreaDensity.FromGramsPerSquareMeter(120) * Area.FromSquareCentimeters(625);
Assert.Equal(Mass.FromGrams(7.5), massOfOneA4Paper);
Assert.Equal(7.5, massOfOneA4Paper.Grams);
}
}
}
2 changes: 1 addition & 1 deletion UnitsNet.Tests/CustomCode/TemperatureDeltaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public void TemperatureDeltaTimesSpecificEntropyEqualsSpecificEnergy()
public void EntropyTimesTemperatureDeltaEqualsEnergy()
{
Energy energy = Entropy.FromKilojoulesPerKelvin(3) * TemperatureDelta.FromKelvins(7);
Assert.Equal(Energy.FromKilojoules(21), energy);
Assert.Equal(21, energy.Kilojoules);
}

[Fact]
Expand Down
41 changes: 41 additions & 0 deletions UnitsNet.Tests/GeneratedCode/TestsBase/AccelerationTestsBase.g.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading