Skip to content

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

Merged
merged 20 commits into from
Jun 26, 2018

Conversation

tmilnthorp
Copy link
Collaborator

Initial go at #391

@@ -136,8 +136,8 @@
{
"SingularName": "ThermImperial",
"PluralName": "ThermsImperial",
"FromUnitToBaseFunc": "x*1.05505585257348e+14",
"FromBaseToUnitFunc": "x/1.05505585257348e+14",
"FromUnitToBaseFunc": "x*1.05967e8",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Owner

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?

Copy link
Collaborator Author

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?

Copy link
Owner

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!

Copy link
Collaborator Author

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!

Copy link
Owner

@angularsen angularsen left a 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.

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}%" );
Copy link
Owner

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.

Copy link
Owner

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%.

Copy link
Collaborator Author

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.

{
public static bool Equals( double value1, double value2, double tolerance = 0.00001, ComparisonType comparisonType = ComparisonType.Relative)
{
if(comparisonType == ComparisonType.Relative)
Copy link
Owner

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

throw new InvalidOperationException("The ComparisonType is unsupported.");
}

public static bool EqualsRelative( double value1, double value2, double tolerance = 0.00001)
Copy link
Owner

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.

Copy link
Collaborator Author

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.


public static bool EqualsRelative( double value1, double value2, double tolerance = 0.00001)
{
double maxVariation = Math.Abs( value1 * tolerance );
Copy link
Owner

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.

Copy link
Collaborator Author

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);
Copy link
Owner

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.

Copy link
Collaborator Author

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)
Copy link
Owner

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.

Copy link
Collaborator Author

@tmilnthorp tmilnthorp Jun 21, 2018

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.

/// <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)
Copy link
Owner

@angularsen angularsen Jun 21, 2018

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.

#409

Copy link
Collaborator Author

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;
Copy link
Owner

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?

Copy link
Collaborator Author

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).

Copy link
Collaborator Author

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!! 😲

Copy link
Owner

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!

Copy link
Owner

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.

Copy link
Collaborator Author

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
Copy link
Owner

@angularsen angularsen left a 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;
Copy link
Owner

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.

Copy link
Collaborator Author

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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow.. Just wow!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know!


namespace UnitsNet
{
public sealed class Comparison
Copy link
Owner

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?

Copy link
Collaborator Author

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!

Copy link
Collaborator Author

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.
Copy link
Owner

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.

Copy link
Collaborator Author

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.

Copy link
Owner

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.

Copy link
Collaborator Author

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",
Copy link
Owner

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!

Copy link
Collaborator Author

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 :)

@tmilnthorp
Copy link
Collaborator Author

Ok. I've updated the documentation. Hope that looks good to you now!

Copy link
Owner

@angularsen angularsen left a 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.

/// </para>
/// <para>
/// Absolute tolerance is defined as the maximum allowable absolute difference between <paramref name="referenceValue"/> and
/// <paramref name="otherValue"/> as a fixed number.
Copy link
Owner

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?

Copy link
Collaborator Author

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.

Copy link
Owner

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.

Copy link
Collaborator Author

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!

/// </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>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

greater or equal

/// <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>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

greater or equal

@@ -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.
Copy link
Owner

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>
Copy link
Owner

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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, what unit?

@tmilnthorp
Copy link
Collaborator Author

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>
Copy link
Owner

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

Copy link
Owner

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.

@tmilnthorp
Copy link
Collaborator Author

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

@angularsen angularsen merged commit 0702d1c into angularsen:master Jun 26, 2018
@angularsen
Copy link
Owner

Awesome! Looks good, nice touch on fixing the old equals methods too.
Nuget 3.101 on the way out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants