thombruce / calculator

Vue Calculator component
https://thombruce.github.io/calculator
0 stars 0 forks source link

Floating Point Precision Errors #1

Closed thombruce closed 3 years ago

thombruce commented 3 years ago

For example, try the calculation 100 * 110%. The result is a 110 plus a teeny decimal value due to floating point precision being unable to accurately represent 0.1.

I started looking at how others have handled the same problem. A lot of good advice in a StackOverflow question I found, but in particular I want to focus on this comment on a similar question which lists some popular libraries: https://stackoverflow.com/a/61764752

In particular, I want to look at MikeMcl's three libraries which he has written a detailed comparison of here: https://github.com/MikeMcl/big.js/wiki#what-is-the-difference-between-bigjs-bignumberjs-and-decimaljs

  1. https://github.com/MikeMcl/big.js
  2. https://github.com/MikeMcl/bignumber.js/
  3. https://github.com/MikeMcl/decimal.js

(Listed in order of increasing size and power.)

The documentation for decimal.js explains that it is used under the hood by Math.js. If we want a lot of power out of these numeric extensions, that's the direction to head in.

Particularly if we extend the calculator to offer scientific calculator features, we are gonna need something like decimal.js!

For more information about how Math.js handles floating point precision...

Fraction.js on GitHub.


If you want to go deeper, Fraction.js is also used by Polynomial.js - read on there to discover how that library also adds support for complex numbers and quaternions.

thombruce commented 3 years ago

Note that how we presently evaluate mathematical expressions is by building the expression as a string, for instance...

'100 * 5 + 6'

...and using the dangerous JavaScript eval() method to evaluate it...

eval(this.currentExpression)

We do this in two places:

  1. Every time we append an operation (appendOperation()); this is called by most of the function buttons, as well as the equals button, and is executed if the present display value is not a "new entry" (!this.newEntry)
  2. Percent calculations

The way that most of the libraries we're evaluating above work is by chaining mathematical functions together like so...

// Math.js
chain(3)
    .add(4)
    .multiply(2)
    .done()  // 14

Math.js can also evaluate arbitrary strings, for example:

// expressions
evaluate('12 / (2.3 + 0.7)')   // 4
evaluate('12.7 cm to inch')    // 5 inch << Thom: I mean look at this one! It handles metric-imperial conversions this way.
evaluate('sin(45 deg) ^ 2')    // 0.5
evaluate('9 / 3 + 2i')         // 3 + 2i
evaluate('det([-1, 2; 3, 1])') // -7

...while this is likely safer than what we have currently, this wouldn't offer any greater precision (which is the point at odds here). That said, it has other methods for returning values of a certain precision if the underlying calculations aren't terribly important.


Yeah, I think we're gonna go with Math.js. I can already envision how to refactor Calculator so that it works with Math.js' chaining. At least, I think I can... I'm thinking we could arbitrarily chain functions together until we call .done()

Caution: Do take a look at the given chain example above... It adds then multiplies, and the given result does not satisfy BODMAS. We may need to find another way...

thombruce commented 3 years ago

What we really want to do is to think about whether or not we actually want to persist with the current method of evaluating a mathematical expression from a string.

Right now... This calculator constructs a string, then performs partial evaluations of that string whilst keeping it in memory. This is... I think... crucial if we want to maintain proper order of operations. Right?

Well, obviously. If we discarded the partial expression, for instance, and simply replaced it with the current value, order of operations would break. But I'm wondering if we need to remember the whole expression thus far, or if it would be safe to do partial summations, right? Based on the current operation (addition, subtraction, multiplication, division)...

For example, in the expression, 2 + 2 + 2 4 + 2, the first two 2s could be safely summed: (4) + 2 4 + 2. The reason that the third 2 would not also be summed, is because our next operation is a multiplication, which should be done first: (4) + (8) + 2. And with that done, it's summation the rest of the way down... 4 + 8 + 2 = 14, the expected result. We haven't strictly operated in BODMAS order, but we have sort of looked ahead when it became relevant.

Y'know, given the relative simplicity of calculators, I do expect this is how they actually work. They are very simple computers performing only binary arithmetic, so an operation button almost certainly almost certainly works with this sort of process. This is in fact what I was initially aiming for before I ventured down a rabbit hole that led me to produce full expression evaluation; in fact, I had some trouble implementing percentages, and the solution is something that behaves somewhat like I've just described.

All of that is to say... we don't need to implement any kind of chaining right now.

Chaining might be useful in future AND for testing purposes (ensuring same value), but simple operations do not require it.

thombruce commented 3 years ago

Hold on this for now. We should address #2 first.