-
Notifications
You must be signed in to change notification settings - Fork 384
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
After update to v5, Equals() does not work as expected when using FluentAssersions' .BeEquevalentTo() #1193
Comments
This is unfortunately by design and it has been discussed at length over years. It seems trivial on the surface, but different quantities have units of different scale where 0 Fahrenheit != 0 Celsius. You you also very quickly run into rounding errors that affect equality. We simply couldn't find a one size fits all for equality, strict equality is at least easy to reason about although not always intuitive. I believe it is best summarized here: |
Having the same problem after updating to v5. UnitsNet become useless for me since I have to deal with different units all around my project. |
@angularsen While i agree that '0 Fahrenheit != 0 Celsius', it also seems apparent to me that for cases where it makes sense (e.g. 0km and 0m) the comparison should yield true. Think of it this way: Length.Zero produces '0 m' and we may have some parsing code which yields results in kilometers. When we write test code, we are forced to do Length.FromKilometers(0) to make the library happy, which looks kind of cumbersome and flaky. One could argue, that we should somehow instruct FluentAssertions to use proper Equals overload with proper ComparisonType and tolerance. I'd would argue though, that having some sensible default tolerance to be used by Equals() w/o parameter would make much more sense. It will keep happy all the folks who were happy with old behavior, while leaving an escape hatch for those who want to specify tolerance/ComparisonType manually. |
I'm very much open to suggestions on a better equality check, but please read up on the background for the strict equality decision first:
Yes, but when multiplying or converting units several times, you would be looking at 0 Celsius == 31.9999999999 Fahrenheit. If we set a default allowed rounding error (option 2 discussed in linked comments) so that 0 Celsius and 31.999.. Fahrenheit are equal, then we no longer satisfy the
If we make the rounding error configurable for those that need it and accept the hash code mismatch, it would probably have to be a global static config. This has its own downsides, such as affecting libraries depending on UnitsNet assuming maybe the default behavior or several pieces of code fighting to manipulate the same config. At least with configurable equality as opt-in, you make an informed choice. It will still not fix the problem that people discover this equality problem the hard way to begin with. Hope this helps! |
Very difficult to do. For example, there are 3 feet in 1 yard. var l1 = UnitsNet.Length.FromYards(1.0);
var l2 = UnitsNet.Length.FromFeet(3.0);
Console.WriteLine(l1.Feet);
Console.WriteLine(l2.Yards); Depending on the order you convert one to the other for comparison you get different results:
How could we assume what tolerance is "good enough" for any particular application? Is BeEquevalentTo the correct choice? Since we implement |
@angularsen I think i understand the rational behind the decision now. Thank you 👍 But, i still needed some sort of solution to use UnitsNet's types with FluentAssertions. I ended up with this code which works just fine:
The only problem here is that i have to duplicate this code for Length, Speed etc. I cannot make this generic because i cannot call |
Ref #1193 In v5, equality changed to strict equality. This meant it became more cumbersome to compare equality for `IQuantity` objects of different units, but similar value when converted to the same unit. - Add interface `IEquatableQuantity` to help compare `IQuantity` objects with a tolerance for error
@dmytro-gokun Proposal in #1215, please take a look if this will work for you. |
@angularsen Yes, that should work. If understand it correctly, with this change, the following code should compile and work as expected:
|
I think it would still be easier for you to use var length = Length.FromMeters(10.01);
var expected = Length.FromMeters(10.0);
// Check within 5% of expected
length.Should().BeInRange(expected - (expected * 0.05), expected + (expected * 0.05));
// Or check with 1mm tolerance
length.Should().BeInRange(expected - Length.FromMilliMeters(1.0), expected + Length.FromMilliMeters(1.0)); |
That, and you always have the option of comparing the value for a known unit. length.Meters.Should().BeApproximately(3.14, 0.01); |
Ref #1193 In v5, equality changed to strict equality. This meant it became more cumbersome to compare equality for `IQuantity` objects of different units, but similar value when converted to the same unit. - Add interface `IEquatableQuantity` to help compare `IQuantity` objects with a tolerance for error
Ref #1193 In v5, equality changed to strict equality. This meant it became more cumbersome to compare equality for `IQuantity` objects of different units, but similar value when converted to the same unit. - Add interface `IEquatableQuantity` to help compare `IQuantity` objects with a tolerance for error Merge IEquatableQuantity into IQuantity<TSelf, Unit, ValueType> interface
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
Related PR merged: #1215 |
Over at EngineeringUnits we have fixed this issue using Fractions to remove any conversion losses. This way it is easy to compare different units. Results: using EngineeringUnits;
using EngineeringUnits.Units;
Console.WriteLine($"{Length.FromMeter(0) == Length.FromKilometer(0)}"); //True
Console.WriteLine($"{Length.FromMeter(1000) == Length.FromKilometer(1)}"); //True
Console.WriteLine($"{Temperature.FromDegreesCelsius(0) == Temperature.FromKelvins(0)}"); //false
Console.WriteLine($"{Temperature.FromDegreesCelsius(0) == Temperature.FromDegreesFahrenheit(32)}"); //True
Console.WriteLine($"{Length.FromFeet(3) == Length.FromYard(1)}"); //True |
After updating to v5 when using FluentAssertions's .BeEquiavalentTo() on classes having UnitNet's types as members, we get false negatives, like:
This worked properly in v4.
As far as I can understand, the reason for this is that .BeEquiavalentTo() uses .Equals() method which is implemented as:
IMO, this is incorrect behavior. 0 km == 0m, 1km == 1000 m etc. Is not that a valid assumption when writing unit tests?
The text was updated successfully, but these errors were encountered: