DEV Community

Cover image for ⛵ Sailing the Choppy Waters of Floating-Point Precision in JavaScript 🔢
Best Codes
Best Codes

Posted on

⛵ Sailing the Choppy Waters of Floating-Point Precision in JavaScript 🔢

Ahoy there, fellow JavaScript sailors! 🚢 Today, we're setting sail on the treacherous seas of floating-point precision. Grab your life jackets, because things might get a little… floaty.

The Floating Conundrum

In the vast ocean of JavaScript, numbers are like the water – they're everywhere. But unlike the predictable H2O, JavaScript numbers can be a bit more… unpredictable. That's because JavaScript uses a single number type: the IEEE 754 double-precision floating-point format. Sounds fancy, right?
Let's explain it a bit.

Understanding IEEE 754

The IEEE 754 standard is a crucial specification for representing floating-point numbers in computers. Let's dive into the details:

  1. Purpose and Background:

    • IEEE 754 was established in 1985 by the Institute of Electrical and Electronics Engineers (IEEE).
    • It addresses issues found in various floating-point implementations, making them more reliable and portable.
    • This standard ensures that computers consistently calculate the same results for the same computations.
  2. Components of IEEE 754:

    • Sign of Mantissa:
      • The sign bit determines whether the number is positive or negative.
      • If the sign bit is 0, the number is positive; if it's 1, the number is negative.
    • Biased Exponent:
      • The exponent field represents both positive and negative exponents.
      • A bias is added to the actual exponent to obtain the stored exponent.
    • Normalized Mantissa:
      • The mantissa is part of a number in scientific notation or a floating-point number.
      • A normalized mantissa has only one 1 to the left of the decimal point.
  3. Special Values:

    • Zero: Represented with an exponent and mantissa of 0. Both +0 and -0 exist but are equal.
    • Denormalized: When the exponent is all zeros, but the mantissa is not, it's a denormalized number.
    • Infinity: Represented with an exponent of all ones and a mantissa of all zeros. Sign distinguishes between positive and negative infinity.
    • Not A Number (NAN): Used to represent errors (exponent field is all ones with a zero sign bit or a non-1 mantissa).

Oh no…

But here's the catch: this format can lead to some unexpected results.

console.log(0.1 + 0.2); // Expected 0.3, but surprise! It's 0.30000000000000004
Enter fullscreen mode Exit fullscreen mode

Why Do We Drift Off Course?

The reason for this numerical oddity lies in how numbers are stored. In JavaScript, all numbers are floating-point numbers, meaning they have a certain amount of space for the digits before and after the decimal point. When we perform calculations, these numbers are converted into binary, and that's where the precision can get a bit… wavy.

Binary systems work great for whole numbers, but for fractions? Not so much. Some numbers that look simple in decimal, like 0.1, are actually infinite repeating fractions in binary (in the same way that 1/3 is 0.33333333...). And since our digital vessels can only hold so much, we end up rounding off, leading to precision errors.

Conversion of 0.1 to Binary
Multiplication by 2:
When converting a decimal fraction to binary, we start by multiplying the fraction by 2.
  • 0.1 * 2 = 0.2
  • Integer part: 0

Decimal Part:
We take the decimal part of the result, which is 0.2, and continue the process.

  • 0.2 * 2 = 0.4
  • Integer part: 0

Repeat the Process:
We continue the process, multiplying the decimal part by 2 at each step.

  • 0.4 * 2 = 0.8
  • Integer part: 0

  • 0.8 * 2 = 1.6

  • Integer part: 1

  • 0.6 * 2 = 1.2

  • Integer part: 1

  • 0.2 * 2 = 0.4

  • Integer part: 0

Binary Representation:
The integer parts obtained in each step form the binary representation. In this case, the binary representation of 0.1 has an infinite repeating pattern, which is 0.0001100110011… and so on.

Steering Clear of the Icebergs

Fear not! There are ways to navigate these choppy waters:

  • Rounding: Use Math.round(), Math.floor(), or Math.ceil() to keep your numbers in check.
  • Fixed Precision: toFixed() can tie down your numbers to a certain number of decimal places.
  • Big Numbers: Libraries like BigDecimal or Big.js can be your lifeboats, offering more precise handling of large or tricky numbers.

Charting the Course Ahead

As we continue our journey through the JavaScript seas, remember that floating-point precision is just one of the many adventures that await. Keep your compass handy (that's your documentation), watch out for the icebergs (those pesky bugs), and always test the waters before you dive in (write those unit tests!).

Happy coding, and may your console logs always be free of unexpected decimals!


And there you have it, a fun yet informative dive into the world of floating-point precision in JavaScript. May your coding journey be smooth sailing from here on out! ⛵


🤖 Yeah, I used AI for some of that. 🧍 No, I didn't use AI for all of it.
Was the sailor theme too much? Let me know in the comments! 😂

Article by Best_codes
https://the-best-codes.github.io/?dev.to

Top comments (5)

Collapse
 
kshyun28 profile image
Jasper Gabriel

As a JavaScript developer, this will always be a recurring caveat that needs proper consideration, especially for applications needing precise calculations.

Have been using a mix of BigNumber and .toFixed() for my needs.

toFixed works well if you only need to handle small decimal places, but for ensuring accuracy for numbers with a lot of decimal places, BigNumber does the job (so far).

Collapse
 
oculus42 profile image
Samuel Rouse

Broader awareness of IEEE 754 limitations can be critical when developing and validating functionality.

One piece that is often overlooked is the limited significant digits that can cause confusion when numbers don't behave as expected. The number of significant digits is fixed and relates to the total amount of detail, not how large or small the number is:

// We can represent very large or small numbers
//  but precision is limited
1.4327194e27         // 1.4327194e+27
1.4327194e27 === (1.4327194e27 + 1)  // true

// Large numbers don't reliably represent decimals
90000000000000.11    // 90000000000000.11
1000000000000000.11  // 1000000000000000.1
2000000000000000.11  // 2000000000000000

// Precision loss doesn't line up with decimal numbers
140737488355327.11   // 140737488355327.11 
140737488355328.11   // 140737488355328.12

// Precision loss can seem inconsistent
14.073748835532711   // 14.073748835532712
14.073748835532811   // 14.073748835532811
Enter fullscreen mode Exit fullscreen mode

Much of the work comes down to understanding the realistic data for your application. If you are processing financial transactions you are unlikely to take an order of one quadrillion dollars less one cent, but if dealing with multiplication you can rapidly exceed the precision limits of the built in number format.

999,999,999,999,999.99   // 1000000000000000

123456.78 * 0.1234567    // 15241.566651426001
Enter fullscreen mode Exit fullscreen mode
Collapse
 
best_codes profile image
Best Codes

Yep. I've been working on a precise feels like calculation that takes data from temperature, humidity, pressure, and wind speed and give me a number — then I discovered that my number was inaccurate because, with this error in JS, and some rather important multiplication and exponents, my number got wacky very quickly.
Thankfully, there are JS libraries like Big.js and math.js to help solve these issues. 😅

Collapse
 
rollcode22 profile image
Roll Code

Great post. I thought the sailor theme was fun.

Collapse
 
best_codes profile image
Best Codes

Awesome! I made some edits to improve the readability of the lists as well.