Have you ever run into the number 0.30000000000000004 or a similar number while coding? Maybe you've noticed that `0.1 + 0.2 !== 0.3`

. This is my attempt at explaining how and why this is the case. But first I'll introduce some context around how I found this bug in my React app. Click here if you want to skip straight to solutions.

## How I found this bug#

Even though a lot of what I've learned so far with FretZone is specific to React, sometimes I encounter bugs in it that have to do not *just* with the fundamentals of JavaScript—but with the fundamentals of *how computers represent information*!

I'm cloning Guitar Pro in the form of FretZone to practice Thinking in React: the process of breaking down UIs into hierarchical components that communicate with each another to represent application state. Somewhere along the way I realized that FretZone could be a fully functional guitar tab editor too! But in order to make the project presentable, I decided I should keep following the process and stub out as much of the UI as possible with JSX and CSS before attempting to make it all work.

The toolbar is an integral part of Guitar Pro's UI. After creating a basic app skeleton, it's more or less where I'm starting to build components. I wasn't sure what to call most of the toolbar controls, so I consulted Guitar Pro's manual.

As expected, the manual includes a breakdown of the toolbar. The checkbox-like buttons (#1 above) to show/hide interface panels were actually among the first parts of the UI I implemented, so next up was the zoom control (#2). Here's a closer look.

With a `select`

on top and an `input[type="range"]`

below to control the open document's zoom level, I thought this would be a cool little component to build out. What are the dropdown options?

A list of mostly numbers, with some extra options at the top and bottom. How to render horizontal separators there will have to wait until I decide on whether to pull in open-source menu components or build them myself.

Interestingly, selecting "Custom..." replaces the dropdown options with an `input[type="number"]`

.

But that can wait until later, too. Here's my first pass at an unstyled `Zoom`

component:

`// Zoom.js`

import React from 'react';

const zoomLevels = [

'Fit to Width',

'Fit to Page',

0.25,

0.5,

0.75,

0.9,

1,

1.1,

1.25,

1.5,

2,

3,

4,

8,

'Custom...',

];

const Zoom = () => (

<div className="Zoom">

<select name="zoom-dropdown" className="Zoom__Dropdown">

{zoomLevels.map((zoomLevel) => (

<option key={zoomLevel}>

{isNaN(zoomLevel) ? zoomLevel : zoomLevel * 100 + '%'}

</option>

))}

</select>

<input type="range" name="zoom-slider" className="Zoom__Slider" />

</div>

);

export default Zoom;

Simple enough, right? Just format all the numbers as percentages, and render the strings as strings. But here's what I got:

Whoa there! What do you mean, "110.0000000000000001%"? 🤔

That looked awfully similar to the dreaded number 0.30000000000000004. I'd heard of it before, but hadn't previously encountered it.

## What's going on here?#

I got a weird output because I'm multiplying `1.1 * 100`

.

1.1 in computer memory actually contains a minuscule **rounding error** that rears its ugly head when it, too, gets multiplied by 100. A common way to demonstrate errors like this is to evaluate `0.1 + 0.2`

in a JavaScript environment. You can test this out in your DevTools console or by running `node`

in a terminal.

`> 0.1 + 0.2`

0.30000000000000004

You might think that JavaScript is broken after seeing this. However, it's not so much that JavaScript is broken, so much as the *numbers themselves* are broken.

## Why?#

### Short explanation#

Humans usually represent numbers in with a decimal (base 10) system. There are plenty of numbers which can't be represented precisely with decimal numbers, like `1/3 = 0.333...`

(repeating). As the fractional digits in numbers like these continue, those digits become less significant to us. So we often make do with approximations like `0.33`

(non-repeating) when performing calculations. In doing so, we lose some precision.

Computers represent numbers with a binary (base 2) system, and likewise represents plenty of numbers imprecisely too.

### Long explanation#

In a given number base, a decimal doesn't repeat (is a terminating decimal) **if and only if** the **denominator** in its fractional representation **shares** **all prime factors** with the base.

JavaScript does all arithmetic in double-precision floating-point format. Also known as binary64, this is the same format as Java and C's `double`

type. It's a way to encode numeric values into 64 bits, with a format that's kind of like scientific notation. If you only care about a certain number of significant digits, then you can represent a **huge** range of numbers in a tiny amount of space by taking those digits and multiplying them by some exponent to get the appropriate magnitude. That is to say, 6.022 x 10^{23} is a lot more compact and easy to comprehend than 602,214,150,000,000,000,000,000.

The technical standard for floating-point arithmetic, IEEE 754, specifies that only up to **52** significant bits (*b* in the formula below) are stored for any binary64 number. 1 bit stores the number's *sign*, and the remaining 11 bits store the exponent (*e*).

So if a number contains infinite repeating digits in its binary representation (like 0.1), its significant digits have to be rounded to fit into 52 bits for a computer to understand it—which introduces rounding errors.

To quote Why 0.1 Does Not Exist In Floating-Point by Rick Regan:

Let’s see what 0.1 looks like in double-precision. First, let’s write it in binary, truncated to 57 significant bits:

0.000110011001100110011001100110011001100110011001100110011001…

_{2}Bits 54 and beyond total to greater than half the value of bit position 53, so this rounds up to

0.0001100110011001100110011001100110011001100110011001101

_{2}In decimal, this is

0.1000000000000000055511151231257827021181583404541015625

which is slightly greater than 0.1.

Any time you're dealing with the number 0.1 in code, it's not really 0.1 at all. Normally the rounding error is too insignificant for us to care. (Try evaluating these numbers in your JavaScript console too!)

`> 0.1000000000000000055511151231257827021181583404541015625`

0.1

`> 0.200000000000000011102230246251565404236316680908203125`

0.2

But if you add or multiply values with rounding errors, those errors can accumulate and give unexpected results. When you're adding `0.1`

and `0.2`

, you're really adding these...

`> 0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125`

0.30000000000000004

And when I'm multiplying `1.1`

by `100`

...

`> (1 + 0.1000000000000000055511151231257827021181583404541015625) * 100`

110.00000000000001

I used this binary converter to obtain the hideous true forms of `0.1`

and `0.2`

, rounding the infinite binary fractions to 52 significant bits.

Like Rick, first I converted `0.2`

to binary which gave `0.001100110011001100110011001100110011001100110011001101`

. Converting back to decimal gave _{2}`0.200000000000000011102230246251565404236316680908203125`

. Note that in binary, the very last significant digit of this number is rounded up.

## Solutions#

Thankfully, JavaScript's built-in data types include functions that help us deal with rounding errors.

### Functions that return strings#

These methods are suitable for when you're done with a number once it's been outputted.

`Number.prototype.toFixed()`

- Rounds
- Argument is # of decimal places to round to
`5.61.toFixed(1)`

→`'5.6'`

`0.0004200.toFixed(5)`

→`'0.00042'`

`Number.prototype.toPrecision()`

- Rounds
- Argument is # of significant digits (ignoring leading 0s if |number| < 1)
`5.61.toPrecision(1)`

→`'6'`

`0.0004200.toPrecision(5)`

→`'0.00042000'`

### Functions that return numbers#

When you want to use calculated values in future calculations, you might want to use these methods.

`Math.trunc()`

**Does not**round`Math.trunc(46.853)`

→`46`

`Math.round()`

- Rounds 🧐
`Math.round(46.853)`

→`47`

`Math.round((0.1 + 0.2) * 100) / 100`

→`0.3`

### Use built-in functions to suit your needs#

While searching for solutions to this bug, I found this function which uses `Math.round()`

to round a number to a specific number of decimal places:

`// https://learnersbucket.com/examples/javascript/learn-how-to-round-to-2-decimal-places-in-javascript/`

let roundOff = (num, places) => {

const x = Math.pow(10, places);

return Math.round(num * x) / x;

};

For a general-purpose rounding function that returns a number, I'd prefer to use the unary `+`

operator with `toFixed`

for readability. If you're familiar with that operator, then this should make plenty of sense:

`> +(0.1 + 0.2).toFixed(1)`

0.3

`// My preferred version of the utility function above`

const roundToDecimalPlaces = (num, places) => +num.toFixed(places);

### For FretZone#

Since I don't mind returning strings for outputting UI labels, I went with `toFixed`

. This solution correctly formats fractional percentages like 25% and 50%, while also truncating any weird rounding errors that arise. (`toFixed`

does round, but the maximum relative rounding error for binary64 is 2^{-53}, so I'm not worried about the ones place accidentally getting rounded up.)

`const formatPercentage = (n) => {`

n *= 100;

if (n >= 1) {

n = n.toFixed(0);

}

return n + '%';

};

// ...

zoomLevels.map((zoomLevel) => (

<option key={zoomLevel}>

{isNaN(zoomLevel) ? zoomLevel : formatPercentage(zoomLevel)}

</option>

));

// ...