Skip to content
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

Add almostEqual which does epsilon comparison for floating point rounding errors. #89

Closed
garo opened this issue Jul 25, 2012 · 9 comments

Comments

@garo
Copy link

garo commented Jul 25, 2012

As javascript stores numbers as doubles inside, one should not just compare optimistically numbers with ==.

For example:
var a = 1000.43 - 1000.0;
var b = 0.43;
a is now 0.42999999999995 and b is 0.43 and thus a == b is false.

The correct (and standard way) to compare these is the epsilon comparison:
Math.abs(a - b) < epsilon, where epsilon is a sufficient small number like 0.000001

I propose a new almostEqual function:
function almostEqual(actual, expected, epsilon, [message]) which compares by comparing Math.abs(actual - expected) < epsilon.

@logicalparadox
Copy link
Member

I think you will find what you are looking for in the chai-stats plugin. I just updated it to ensure compatibility with the 1.1.x chai release. It uses a precision indicator to indicate the decimal places. More info on how it is used is in the readme.

assert.almostEqual(a, b, 2);

@garo
Copy link
Author

garo commented Jul 26, 2012

Thanks for fast response. I looked on the chai-stats and in my opinion it's not that good choice. Here's why I think so:

  1. It simplifies the epsilon argument into 'decimals' in such way that it limits the usage. In my case I had to test if a function sets the current timestamp into a variable. I can test these easily with epsilon comparison:
function functionToBeTested() {
  return (new Date().getTime());
} 

function test() {
  var a = functionToBeTested();
  assert.ok(Math.abs(a - (new Date().getTime()) < 50);  // Make sure that the timestamp is close enough to current time.
  // or with my suggestion:
  // assert.almostEqual(a, (new Date().getTime()), 50);
}

This example is not suitable if the epsilon is simplified into the 'decimal' arguments.

  1. The other reason comes because the nature of JavaScript Number type: they're always a 64 bit double floating point. And because floating point math brings rounding errors, they should not be compared with ==, but instead one should use epsilon comparison. The fact that most people use == to compare javascript numbers and get away with it is because they usually work only with number values which can be accurately represented (ie. integers). Because of this fact I believe that every javascript assertion library should contain some sort of assertEqual implementation which allows epsilon comparison and their documentation also should explain why it's important to use it. It's just as important as the assert.equal function used for comparing string and it should not be under a separate module because people won't find it.

It's also useful to link into this article: http://floating-point-gui.de/

@logicalparadox
Copy link
Member

We actually already have this, now that I think about it... https://github.com/chaijs/chai/blob/master/lib/chai/core/assertions.js#L1027-1047

But... we don't have an assert equivalent. I can add that in.

@Klortho
Copy link

Klortho commented Jun 12, 2016

Unfortunately, that's not what to OP requested at all.

If anybody comes across this from Google like I did, I wrote a function for this, see this gist: assert-close-enough. I probably didn't follow the chai api correctly, but it works for me.

Edit: I misread the request, but the gist I wrote might be useful. It automatically determines epsilon, so you don't have to worry about the scale of the numbers you are comparing.

@meeber
Copy link
Contributor

meeber commented Jun 12, 2016

@Klortho Chai offers a .closeTo assertion (aliased to .approximately) which seems to be what the OP was asking for unless I'm misunderstanding the issue:

// expect interface
expect(1000.43 - 1000.0).to.be.approximately(0.43, 0.000001);
expect(1000.43 - 1000.0).to.be.closeTo(0.43, 0.000001);

// assert interface
assert.closeTo(1000.43 - 1000.0, 0.43, 0.000001);

@Klortho
Copy link

Klortho commented Jun 12, 2016

Well, closeTo is close, but it's a fail, I'm afraid. (Sorry I couldn't resist).

What's needed (still, IMO) is a comparison with the correct scale. Your example works fine if the developer knows ahead of time the values to expect. But if those are computed, and at some completely different order of magnitude, it could easily fail. For example:

it('should work for every SI prefix', function() {
  [ 'micro', 'milli', 'kilo', 'mega', ...].forEach(function(prefix) {
    const size = unitUnderTest.size(prefix);
    assert.closeTo(size, lookup(prefix), 0.000001);
  });
});

That's pretty contrived, but the point is that there's no reason the developer should have to even think about or guess. We know what the precision is of JS numbers, and can calculate what is "close enough" from the input values.

@meeber
Copy link
Contributor

meeber commented Jun 12, 2016

@Klortho Gotcha. If this is something you think should be in Chai core, please open a new issue. This one is nearly 4 years old, and (despite the title) the OP was asking for something a little different than you are. In particular, they wanted the ability to specify the allowed delta. Their first example is the one I borrowed from, and their second example required being able to provide a specific delta in milliseconds for their timestamp comparison, both of which are solved by the current closeTo implementation. Thanks!

@Klortho
Copy link

Klortho commented Jun 13, 2016

Yes, you are right -- I read as far as "epsilon" and skimmed the rest. I do wish chai had this function, but if no one else has asked for it in all this time, I'll just keep using my local customization. Thanks!

@agrohs
Copy link

agrohs commented Dec 31, 2020

Why is this issue closed??

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

No branches or pull requests

5 participants