-
Notifications
You must be signed in to change notification settings - Fork 391
Relative and absolute equality #451
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
Relative and absolute equality #451
Conversation
UnitsNet/UnitDefinitions/Energy.json
Outdated
@@ -136,8 +136,8 @@ | |||
{ | |||
"SingularName": "ThermImperial", | |||
"PluralName": "ThermsImperial", | |||
"FromUnitToBaseFunc": "x*1.05505585257348e+14", | |||
"FromBaseToUnitFunc": "x/1.05505585257348e+14", | |||
"FromUnitToBaseFunc": "x*1.05967e8", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are actually 4 definitions: Therm EC/US/UK and BTU
https://en.wikipedia.org/wiki/Therm#Definitions
ThermImperial may be misleading in name, but refers to Therm UK.
There is already a unit for BritishThermalUnit (BTU) further up here.
So I think it is already correct, as is. Do you agree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did assume it was therm(UK). I thought that was therm_39, but looks like I'm completely wrong there. It caught my eye immediately though because the scale is completely off (e14).
If you look at Wikipedia, it should be 1.05505585257348e8, not 1.05505585257348e14. Looks like the decimal on Wikipedia got overlooked :)
So just to make sure I'm not missing anything, 1.05505585257348e8 seems correct, do you agree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I missed that part :-D Yup, should be e8
!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will change it to that then!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of things on the work so far, but I like where this is headed.
UnitsNet.Tests/AssertEx.cs
Outdated
double difference = Math.Abs(expected - actual); | ||
double percentDifference = (difference / actual) * 100.0d; | ||
|
||
Assert.True( areEqual, $"Values are not equal within relative tolerance: {tolerance * 100.0d}%\nExpected: {expected}\nActual: {actual}\nDiff: {percentDifference}%" ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use format P2 as in {0.0354321:P2}
to get "3.54%"
string including the %
symbol. It also uses the right symbol for the current culture.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would perhaps argue that percentage should be relative to expected
and not actual
, since you don't really know what actual is, but expected is .. well.. what you expect :-) If expected is 10
and actual is 0.000001
, then a difference of ~10
becomes a huge percentage instead of having an error of ~100%
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't know you could do the P2 formatting outside of a ToString. Nice! I'll update that. And I agree that it should be against the expected.
UnitsNet/Comparison.cs
Outdated
{ | ||
public static bool Equals( double value1, double value2, double tolerance = 0.00001, ComparisonType comparisonType = ComparisonType.Relative) | ||
{ | ||
if(comparisonType == ComparisonType.Relative) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A switch
can be slightly more readable when comparing the same value multiple times and returning a value based on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
UnitsNet/Comparison.cs
Outdated
throw new InvalidOperationException("The ComparisonType is unsupported."); | ||
} | ||
|
||
public static bool EqualsRelative( double value1, double value2, double tolerance = 0.00001) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The xmldoc should explain that the tolerance is relative to value1
, which is kind of important I think.
Consider renaming value1
to referenceValue
, since we must choose a reference to compare relative to. The other could be named value
maybe. Perhaps there are some naming conventions on this sort of thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. I've updated this and added documentation that I hope is clear.
UnitsNet/Comparison.cs
Outdated
|
||
public static bool EqualsRelative( double value1, double value2, double tolerance = 0.00001) | ||
{ | ||
double maxVariation = Math.Abs( value1 * tolerance ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should throw if tolerance is < 0
, even if it would work the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I was going to initially say <= 0 but I think it's perfectly valid to request being exact if desired.
#region PetawattPerCubicMeter | ||
|
||
/// <inheritdoc cref="PowerDensity.FromPetawattsPerCubicMeter(UnitsNet.QuantityValue)" /> | ||
public static PowerDensity PetawattsPerCubicMeter(this int value) => PowerDensity.FromPetawattsPerCubicMeter(value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider moving the PowerDensity units to a separate PR, even if that is probably why you started working on the abs vs relative comparison to begin with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is indeed why I started :) I just forgot to remove it. Good catch
/// <param name="tolerance">The comparison tolerance.</param> | ||
/// <param name="comparisonType">The comparison type: either relative or absolute.</param> | ||
/// <returns>True if the difference between the two values is not greater than the specified relative or absolute tolerance.</returns> | ||
public bool Equals(VolumeFlow other, double tolerance = 0.00001, ComparisonType comparisonType = ComparisonType.Relative) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tolerance probably needs to be of type VolumeFlow
, since the consumer may be working with units of entirely different orders of magnitude than the base unit - so the default of 0.00001
may be way too small or way too large.
A second option is to rely on the fact that we keep track of the Unit
the quantity was constructed with, so we could pick one of the quantities' Unit
and convert both quantities to a numeric value of that unit, then compare that with a default numeric tolerance. So when comparing a 1 nanometer with 2 nanometers, then the tolerance would 0.000001 nanometers by default rather than 0.000001 meters. The pitfall here is that it matters WHICH value you choose the unit for, as you get very different behavior when comparing 1 kilometer to 1e12 nanometers vs comparing 1e12 nanometers to 1 kilometer. In this particular example, I would expect this
to be the reference - which I guess is intuitive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I like option 2 better. Absolute/relative can be a bit confusing and keeping it a double makes it easier to think of as a "percentage". Being typed would make sense for absolute differences, but since this is a generic method I think it's better as a double.
I made this in the same thinking of String.Equals where you can pass in the comparison type (ignore case, etc.).
I agree the reference being [this] is intuitive.
…lacing manual percentage calculation with format strings.
…g isn't done when not using base units. [this] will be the reference for relative units.
…ter with name "value"
UnitsNet/Comparison.cs
Outdated
/// <param name="tolerance">The absolute or relative tolerance value.</param> | ||
/// <param name="comparisonType">Whether to use tolerance as an absolute or relative tolerance.</param> | ||
/// <returns></returns> | ||
public static bool Equals(double referenceValue, double value, double tolerance, ComparisonType comparisonType) | ||
public static bool Equals(double referenceValue, double otherValue, double tolerance, ComparisonType comparisonType) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WRC target is a big pain in the ass, I've contemplated dropping the support for it, but we have a Microsoft package that depends on it which is kind of cool.
I have created an issue to generate a separate code base for WRC, to avoid all the #if defs, which I think will be an improvement since most people don't care about WRC anyway. Then hopefully one will meet less friction when adding new stuff, since adding extra code like operator overloads would by default not be included in WRC.
The downside being a sh*tload more files generated for every unit etc added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did see that, and considered picking that up :)
@@ -42,11 +42,11 @@ public class MassTests : MassTestsBase | |||
|
|||
protected override double KilotonnesInOneKilogram => 1E-6; | |||
|
|||
protected override double LongTonsInOneKilogram => 0.000984207; | |||
protected override double LongTonsInOneKilogram => 9.842065276110606e-4; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is getting a bit crowded and not so easy to review. Could you perhaps pull out the bits not directly related to absolute/relative functionality in a separate PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately all of these changes are so the tests pass. These validation numbers were off quite a bit when I changed the tests to validate by % tolerance (not precise enough).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So in other words, they're unfortunately all related to absolute/relative functionality! Especially since I have switched the tests over to relative tolerance by default.
Without the mass tests change, mass tests fail with the following (MegatonnesInOneKilogram in this example):
Message: Values are not equal within relative tolerance: 0.0010%
Expected: 1E-06
Actual: 1E-09
Diff: 99.9000%
As you can see, it was a difference of 1e-6, which was inside the 1e-5 tolerance, but it was off by almost double!! 😲
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha 😆 it's so good to finally fix this!
I understand about the precision changes, no problem!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this getting ready now? I might find some time tomorrow to review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup! I'm done making changes.
Whitespace fix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! Phew, this is a ton of good work! You really did a huge job going through all these conversions and finding so many discrepancies in the test cases. Please see my comments.
|
||
protected override double YearsInOneSecond => 3.1689e-8; | ||
protected override double YearsInOneSecond => 3.170979198376458e-8; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This value was different than Years365 and the average year instead of 365 days.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you saying it is supposed to be different? Both are defines in the json files as x36524*3600, so I am not sure how they could be?
|
||
protected override double MegapoundsInOneKilogram => 2.2046226218487757e-6; | ||
|
||
protected override double MegatonnesInOneKilogram => 1E-6; | ||
protected override double MegatonnesInOneKilogram => 1E-9; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow.. Just wow!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know!
UnitsNet/Comparison.cs
Outdated
|
||
namespace UnitsNet | ||
{ | ||
public sealed class Comparison |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this class static
? Should it be internal
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought I tried and Windows Runtime Component yelled at me. I'll double check because it should definitely be static. Not so sure about internal. Can be used anywhere if someone wishes!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to be fine with static. I'll leave it as public though if that's ok with you?
@@ -790,6 +790,29 @@ public override bool Equals(object obj) | |||
return AsBaseUnitMetersPerSecondSquared().Equals(((Acceleration) obj).AsBaseUnitMetersPerSecondSquared()); | |||
} | |||
|
|||
/// <summary> | |||
/// Compare equality to another Acceleration within the given absolute or relative tolerance. | |||
/// Relative tolerance is when the difference between the two quantities is not greater than the scale of the values compared to the tolerance. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about:
Relative tolerance is defined by a fraction of this quantity, so a value of 0.01 means +/- 1% tolerance.
Absolute tolerance is defined by the max difference between this quantity and in the of this quantity. So if this quantity was constructed with 100 cm, then a tolerance of 0.01 means a tolerance of +/- 1 cm.
When writing this, I realize that I'm not sure this behavior for absolute tolerance is intuitive enough and whether we should have a separate method for absolute equality that instead take an Acceleration
as tolerance? The relative tolerance implementation is fine I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think having an example is good here. Makes it easier to understand. I'll add that.
As for making the method typed I'm not so sure. When we want to implement an IQuantity interface, having the generic method that takes a double is the only way to make it reusable as an interface method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, but that's the part I'm not sure about. Will it be intuitive enough to expose an EqualsAbsolute()
method generically? You don't necessarily know what unit you are working with, something like this:
public void MakeImportantDecisionIfTheseTwoArePrettyMuchEqual(Length a, Length b)
{
// What exactly is the allowed error here? Are we working with Meters here? Nanometers? Lightyears?
if (a.EqualsAbsolute(b, 0.001)) DoTheImportantThing();
}
It doesn't get better if you substitute Length
with IQuantity
, then it could as well be oranges and apples let alone how many of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is true with the interface, but generally I'd expect a check, much like Equals(Object) does. If the types are different it would just always return false. But all debatable :)
Are you saying you'd like to see:
var a = Length.FromMeters( 5.0 );
var isEqual = a.EqualsAbsolute( Length.FromMeters( 10.0 ), Length.FromNauticalMiles( 100000 ) )
"FromUnitToBaseFunc": "x*98066501.9960652", | ||
"FromBaseToUnitFunc": "x*1.01971619222242E-08", | ||
"FromUnitToBaseFunc": "x*9.80665e7", | ||
"FromBaseToUnitFunc": "x/9.80665e7", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice that you are making this consistent with the guideline!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the tests failed due to relative checks, I couldn't know if the conversion was wrong or the test, so I did both :)
… other, $quantityName maxError)
Ok. I've updated the documentation. Hope that looks good to you now! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a couple of minor things. I'm still a bit on the fence on the absolute equality though, it feels a bit weird passing in an absolute tolerance without a unit. I honestly don't know if it is the base unit or if it is the constructed unit, without looking in the source code. It might be fine if the xmldoc clearly states the behavior, though, but I think it will catch some people off guard whereas requiring the unit will make them aware that this is a thing to consider.
UnitsNet/Comparison.cs
Outdated
/// </para> | ||
/// <para> | ||
/// Absolute tolerance is defined as the maximum allowable absolute difference between <paramref name="referenceValue"/> and | ||
/// <paramref name="otherValue"/> as a fixed number. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed number, but in what unit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case it's just a double, so there is no unit. But when you're looking at the Equals on a quantity, the reference value is "this" so it's a fixed number of "this" unit.
var a = Length.FromMeters( 100.0 );
var b = Length.FromInches( 5.0 );
a.Equals( b, 0.001 )
In this case it's absolute of a. So within 0.001 meters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay sure, that is one way to look at it. I will still argue that there is a unit in play here and it is the constructed unit of a
, which is the most sensible choice by all means! However, I would have to read the current xmldoc more than once to understand what will actually happen, since there is conversions going on here and you are naturally not comparing the values 100 and 5 head on. If the xmldoc rather said the tolerance was in the referenceValue
's Unit
then at least in my eyes it would be much more intuitive. I guess we just have different perspective on this and read it a bit differently.
So for me, a slight change in wording of xmldoc would make it a lot more intuitive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think changing the wording definitely makes sense regardless!
UnitsNet/Comparison.cs
Outdated
/// </summary> | ||
/// <param name="referenceValue">The reference value which the tolerance will be calculated against.</param> | ||
/// <param name="otherValue">The value to compare to.</param> | ||
/// <param name="tolerance"></param> | ||
/// <param name="tolerance">The relative tolerance. Must be greater than 0.</param> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
greater or equal
UnitsNet/Comparison.cs
Outdated
/// <param name="otherValue">The value to compare to.</param> | ||
/// <param name="tolerance">The absolute or relative tolerance value.</param> | ||
/// <param name="comparisonType">Whether to use tolerance as an absolute or relative tolerance.</param> | ||
/// <param name="tolerance">The absolute or relative tolerance value. Must be greater than 0.</param> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
greater or equal
UnitsNet/Comparison.cs
Outdated
@@ -70,7 +81,8 @@ public static bool EqualsRelative(double referenceValue, double otherValue, doub | |||
|
|||
/// <summary> | |||
/// Checks if two values are equal with a given absolute tolerance. | |||
/// Absolute tolerance is when the absolute difference between the two values is not greater than the tolerance value. | |||
/// Absolute tolerance is defined as the maximum allowable absolute difference between <paramref name="referenceValue"/> and | |||
/// <paramref name="otherValue"/> as a fixed number. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same, what unit?
/// </summary> | ||
/// <param name="other">The other quantity to compare to.</param> | ||
/// <param name="tolerance">The comparison tolerance.</param> | ||
/// <param name="tolerance">The absolute or relative tolerance value. Must be greater than 0.</param> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
greater or equal
/// </para> | ||
/// <para> | ||
/// Absolute tolerance is defined as the maximum allowable absolute difference between this quantity's value and | ||
/// <paramref name="other"/> as a fixed number. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same, what unit?
Updating the documentation on the Equals method in quantity classes |
/// </para> | ||
/// </summary> | ||
/// <param name="other">The other quantity to compare to.</param> | ||
/// <param name="tolerance">The absolute or relative tolerance value. Must be greater than 0.</param> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
greater or equal to 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also update Comparison
with the same documentation updates.
I will also obsolete Equals($quantityName other, $quantityName maxError) since we have a method that does relative and obsolete now. I'll also change the Equals(Object) implementation |
…als methods that were scaling to base units when unnecessary. Whitespace fixes.
Awesome! Looks good, nice touch on fixing the old equals methods too. |
Initial go at #391