# C# vs JavaScript: Numbers

*This post belongs to the C# vs JavaScript series.*

by Nicklas Envall

That JavaScript is not good with decimal fractions is well-known. However, this regularly used phrase leads many developers to believe that the problem magically only exists in JavaScript. Although admittedly, JavaScript has only one number type that can hold fractions. Thus, the problem is more prominent in JavaScript than in a language with many number types like C#. Yet, the issue itself existed long before JavaScript. So before we compare the two languages' approaches to numbers, let's teleport ourselves back to a time where our ancestors depended on repetitive drawings. A time where to write down that you had ten dogs, you would have to draw a dog ten times.

As you might expect, drawing each object that you want to represent repetitively is inefficient. So our ancestors later realized that it's less effort to use a stick to symbolize **one**. Consequently, they then only had to draw one dog and ten lines. But as the world evolved, it became essential for trade and communications to invent even better ways of *counting* and doing *computations*. Evidently, we needed a way to represent numbers. We needed a *number system*. As a result, we humans created many throughout different parts of the world, like Egypt, Greece, China, and much more.

With this in mind, let's look to ancient Rome. Because you see, the Romans invented the Roman numeral system, that offered seven symbols:

`I = 1 V = 5 X = 10 L = 50 C = 100 D = 500 M = 1000`

Their system is base-10 and was * originally* purely additive. It being additive meant that the symbol's position did not matter. Instead, because each symbol holds a fixed value, you would add all those values together to get the sum. For example,

`LXX`

equals `70`

, and by inspecting it, we see that each `X`

still means `10`

even though they have different positions. We can view it as, `50 + 10 + 10 = 70`

.The system itself had flaws that made it hard to do more advanced mathematics, like multiplication and division. Yet despite its flaws, it was adapted throughout Europe and continued being used even after the Roman empire's decline. Though, note that it got improved with time. For instance, *subtractive notation* got introduced where you got abbreviations like `IV`

. Lastly, the Roman numeral system is used to this day, although mainly only for artistic purposes.

The Hindu−Arabic/Indo-Arabic * positional* number system is what's widely used throughout the world today. It was developed in India and used for a long time in the Middle East before being picked up in Europe around 1200-1600. It provides ten symbols:

`{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }`

Contrary to the Roman system, it has a symbol for *zero*, which enables *positional notation*. For example, as you see in the example below, when we reach the number nine, we reset a digit on the right and add a "1" on the left, and as a result, get ten.

`... 8 9 10 <-- reset digit on right and add 1 11 12 ...`

Because the decimal system is positional, a digit is multiplied by `10^i`

. Where `i`

equals the digit's position in the number, and `10`

is the *base* of the number system. Take `5641,39`

for example:

`5 * 10^3 = 5000 6 * 10^2 = 600 4 * 10^1 = 40 1 * 10^0 = 1 3 * 10^-1 = 0.3 9 * 10^-2 = 0.09`

Most of us use this system today. But despite all the progress we've made with it, who knew that the same line that our ancestors drew would be what helped bridge the gap between arithmetics and electricity, with its' friend `0`

.

## Binary Numbers

Computers operate on ones and zeros, and their language is *binary*. The binary numeral system is positional and base-2, and as a result, it provides two symbols, `0`

and `1`

. These binary digits (bits) are used together to forge binary numbers, and the example below shows `8`

to `12`

in binary:

`... 1000 1001 1010 1011 1100 ...`

We can use the *positional notation method* to convert binary to decimal. For instance, the binary representation of the decimal number `9`

is `1001`

as proven below:

`1 * 2^3 = 8 0 * 2^2 = 0 0 * 2^1 = 0 1 * 2^0 = 1`

Then you can use division to convert decimal into binary because division "undoes" multiplication.

`9 / 2 = 4 remainder = 1 4 / 2 = 2 remainder = 0 2 / 2 = 1 remainder = 0 1 / 2 = 0 remainder = 1`

With this in mind, we now have a way to represent *positive integers*. Subsequently, if we utilized two's complement, then we could represent *negative integers*. But how do we store a *ratio* between two numbers? For example, `1/2`

(`0.5`

in *decimal fraction* form). More mathematically speaking, I'm referring to *rational numbers* and *irrational numbers*.

**Rational numbers**: either*stop*or*repeats*a pattern. So`4.5`

is rational because it's finite, while`1.333333...`

is rational because it repeats a pattern.**Irrational numbers**: never stop, and do not repeat a pattern. Examples of irrational numbers are`π`

,`√2`

, and`e`

.

One way to store fractions is to use the *fixed point system*. We divide the bits into three fields:

- 1-bit for the sign of the number.
- Bits for the binary number before the binary point.
- Bits for the binary number after the binary point.

For example, with 32 bits, the ratio `22/4`

(5.5) could look like:

`0 | 000000000000101 | 1000000000000000`

Because:

`1 * 2^2 = 4 0 * 2^1 = 0 1 * 2^0 = 1 1 * 2^-1 = 0.5`

In addition to the fixed point system, we now have a more common way. That is to say, with the introduction of the IEEE standard 754, we got another method to represent fractions, the *floating-point format*. It gives us different floating-point formats like *Single-Precision* (32 bits) and *Double-Precision* (64 bits), and in short, a standard for *floats*.

How a floating-point gets stored is similar to *scientific notation*, which is a way to express numbers. To refresh our memory, we have the scientific notation formula below, where `S`

stands for *significand* and `E`

for *exponent*:

`x = S * 10^E`

So, for instance, `553.2`

in scientific notation is `5.532 * 10^2`

, and as we see, we had to move the decimal point. After the decimal point gets moved, it's in its so-called *normalized form*. Though, it's moved because the significand is always `1 <= S < 10`

by convention. So, the decimal point "floats" to the correct position, and that's why we ended up with the name *floating-point*.

Recognizing that our computers work with binary, we must now go from using *decimal scientific notation* to *binary scientific notation*, where the significand should be `1 <= S < 2`

, and the base 2. So the ratio `22/4`

(5.5) is `101.1`

in binary. In binary scientific notation, it would be `1.011 * 2^2`

. As a result, we now have a *binary point* instead of a decimal point.

Then to store this value, we end up with the following formula:

`(-1)^s * 1.f * 2^e-b`

Where:

`s`

stands for sign, and indicates if the number is negative or positive.`f`

is the significand fraction.`e`

stands for exponent.`b`

stands for bias.

Then by using the so-called *single-precision* format, we could divide it into three fields:

- A 1-bit field for the sign of the number.
- An 8-bit field for the exponent.
- A 23-bit field for the significand.

There's more to floats, and I've intentionally left out parts because we could write an entire book on the subject. Instead, let's shift our attention to so-called *rounding issues*, before we start comparing JavaScript and C#.

### Rounding Issues

Our binary numbers can continue forever, while our computers only have a finite amount of bits available. So to make these kinds of binary numbers storable in a field, we need to *round* the significand by omitting some bits. Consequently, we cannot represent every number 100% accurately, which sadly may lead to unexpected results. Because whether we like it or not, rounding essentially entails making a less precise version of the original number. In addition, many decimal numbers have an infinite representation in binary.

For example, `0.1`

in decimal continues forever in its binary representation. Why would `1/10`

repeat forever in binary? The answer is that in base-2, rationals that have denominators that are powers of 2 stop. Else, they'll be infinite. To see for yourself, divide `1`

with `1010`

using long division. Because by doing that, you'll quickly see that a never-ending pattern emerges:

`0.0001100110011001100110011001100110011...`

Then, in a double-precision floating-point, we would have a 53-bit field for the significand. So we would store the following:

`0.0001100110011001100110011001100110011001100110011001101`

Subsequently, if we would convert that to decimal, then we would get:

`0.10000000000000000555`

As we see, our computer has not stored `0.1`

. Instead, it has stored an *approximation* of the value, which in this case is higher than `0.1`

.

## C# Numbers

So far in this series, there has been a pattern of first looking at JavaScript and then C#. However, I will now break that pattern because C# has more number types.

### Integers

Integers are *whole numbers*, including negative numbers, without fractions. So `{ ... −3, −2, −1, 0, 1, 2, 3, ... }`

are integers, while `1/2`

or `0.75`

aren't since they're fractions. We have different types of integers in C#, and the more bits they have, the more numbers they can represent.

- Byte: 8-bit
- Short: 16-bit
- Int: 32-bit
- Long: 64-bit

For example, declaring three different integer variables and then adding `a`

and `b`

together works as you probably would expect:

`int a = 1; int b = 2; int c = a + b; c; // 3`

But as we know, integers cannot hold fractions. So what would happen if we divide `6`

by `4`

? Since mathematically speaking, that would give us `1.5`

, and `4 / 6 = 0.66...`

.

`6 / 4; // 1, despite being 1.5 4 / 6; // 0, despite being 0.66...`

As you see, that's not the case in C#. Because *integer division always results in an integer* by *truncating* (removing) the remainder. Thus if we want to get the remainder of `6 / 4`

, then we could do something like this:

`int dividend = 6; int divisor = 4; int quotient = dividend / divisor; // automatically truncated int remainder = dividend - quotient * divisor; remainder; // 2`

But we don't need to do this every time. Instead, luckily, the **remainder operator** `%`

saves us from this. With which you do `dividend % divisor`

, and you'll get the remainder. It essentially works the same as in JavaScript, and you can read how to use the remainder operator in JavaScript here.

`6 % 4; // 2 4 % 6; // 4`

Lastly, a note on memory, for example, an `int`

can store 32 bits, and that means it can represent `2^32`

(`4294967296`

) different numbers. If we do `int.MaxValue`

, we get `2147483647`

. Secondly, with `int.MinValue`

, we get `-2147483648`

. But, `2147483648 + 2147483647`

equals `4294967295`

not `4294967296`

. However, with `0`

, we have `4294967296`

different numbers in the end. There are also so-called *unsigned* number types, and in short, those types only handle positive numbers and therefore have 1 bit extra.

### Floating-Point Types

C# provides the following floating-point types:

- Float: 32-bit
- Double: 64-bit
- Decimal: 128-bit

From the introduction, we know that the IEEE standard provides two key formats, Single-Precision (`float`

) and Double-Precision (`double`

). But, `decimal`

is new. Although it essentially works the same, it gets interpreted as base-10. Consequently, `decimal`

avoids the rounding errors when working with decimal data that the binary floating-points `float`

and `double`

suffers from:

`float num1 = 0.22f + 0.7f; double num2 = 0.1d + 0.2d; num1; // 0.91999996 num2; // 0.30000000000000004 decimal num3 = 0.22m + 0.7m; decimal num4 = 0.1m + 0.2m; num3; // 0.92 num4; // 0.3`

For this reason, it's more viable for banking. Still, keep in mind that if it's a fraction that reoccurs in base-10, then neither `decimal`

nor `double`

can handle it:

`decimal oneThird = 1m / 3m; decimal one = oneThird + oneThird + oneThird; one; // 0.9999999999999999999999999999`

## JavaScript Numbers

JavaScript's number is a double-precision 64-bit binary format IEEE 754 value. As a consequence, we can get strange results when working with decimal fractions. But, in the introduction, we saw that this is nothing unique to JavaScript. Nevertheless, many other languages do have more built-in solutions, like types, to mitigate those issues.

JavaScript can still *represent* integers, despite it not having an integer type, since `1 === 1.0`

. Just remember that it's still a floating-point:

`10 / 4; // 2.5 Math.floor(10 / 4); // 2`

Then, we should note that it can only *exactly* and *safely* represent all integers from `-9007199254740991`

(`Number.MIN_SAFE_INTEGER`

) to `9007199254740991`

(`Number.MAX_SAFE_INTEGER`

). Because strange things will start to happen if we go outside of that range:

`Number.MIN_SAFE_INTEGER - 4; // -9007199254740996 🤔 Number.MIN_SAFE_INTEGER - 3; // -9007199254740994 Number.MIN_SAFE_INTEGER - 2; // -9007199254740992 🤔 Number.MIN_SAFE_INTEGER - 1; // -9007199254740992 Number.MIN_SAFE_INTEGER; // -9007199254740991 Number.MIN_SAFE_INTEGER + 10; // -9007199254740981 Number.MIN_SAFE_INTEGER + 10**12; // -9006199254740991 (-100).toPrecision(10); // "-100.0000000" (0).toPrecision(10); // "0.000000000" (100).toPrecision(10); // "100.0000000" Number.MAX_SAFE_INTEGER - 10**12; // 9006199254740991 Number.MAX_SAFE_INTEGER - 10; // 9007199254740981 Number.MAX_SAFE_INTEGER; // 9007199254740991 Number.MAX_SAFE_INTEGER + 1; // 9007199254740992 Number.MAX_SAFE_INTEGER + 2; // 9007199254740992 🤔 Number.MAX_SAFE_INTEGER + 3; // 9007199254740994 Number.MAX_SAFE_INTEGER + 4; // 9007199254740996 🤔`

More practically speaking, we have a `Number`

object that has properties like `Number.MAX_SAFE_INTEGER`

and `Number.POSITIVE_INFINITY`

. It also has static methods like `Number.isInteger()`

and `Number.isSafeInteger()`

.

Not to mention the `Number.prototype`

object, which provides instance methods, like `Number.prototype.toFixed()`

that we can use to help us with *rounding issues* in some cases:

`0.1 + 0.2; // 0.30000000000000004 (0.1 + 0.2).toFixed(1); // "0.3"`

Or why not use it to sneak peek at what JavaScript is actually storing for `0.1`

?

`(0.1).toFixed(100); // "01000000000000000055511151231257827021181583404541015625000000000000000000000000000000000000000000000"`

Finally, the `Number`

and the `Number.prototype`

objects are sometimes not sufficient, so then you'd probably be using the built-in `Math`

object. But I'll leave that for a future article.

### BigInt

In reality, ECMAScript has two built-in numeric types: `number`

and `BigInt`

. It makes sense to use `BigInt`

when you want to use integers, and the safe integer range from `-9007199254740991`

to `9007199254740991`

is not sufficient.

To create a `BigInt`

, suffix your integer with `n`

:

`9007199254740993; // 9007199254740992 9007199254740993n; // 9007199254740993n`

A `BigInt`

cannot hold fractions, just like `int`

in C#:

`6 / 4; // 1.5 6n / 4n; // 1`

Lastly, most comparisons between `number`

and `BigInt`

are fine. But, be aware that you cannot mix arithmetic operations with `number`

and `BigInt`

. Although, you can explicitly convert them beforehand at your own risk.

`1n + 2; // TypeError: can't convert BigInt to number 3n > 1; // true 3n < 1; // false 3n === 3; // false 3n == 3; // true`

### The more number types the better?

Douglas Crockford has both in talks and the book "How JavaScript Works," said that it's a good thing that JavaScript only had one numerical type originally. Then going on to say that `BigInt`

was a mistake because it was already possible to achieve the same with libraries. Therefore, it's not justifiable to lose that simplicity in the language by implementing `BigInt`

. Other languages like C# don't have the same simplicity with their `byte`

, `short`

, `int`

, `float`

, and `double`

, which may lead to hard-to-reason code and bugs.

Now, I've seen online that people argue that natively having `BigInt`

helps us avoid libraries and makes it more performant. Still, I'm leaning towards the idea that having one number type is better for simplicity reasons. What do you think?

Nevertheless, Crockford also points out that even though JavaScript's number is a good idea, it's still the wrong type. It should've been a decimal floating-point, not a binary one. The reasoning for this is that binary floats cannot represent all decimal digits correctly. Yet, those digits are the ones we humans care about the most.

I think it's some interesting points that give us some perspective on the whole JavaScript versus C# number mindset.

## Converting Numbers

So far, we've covered numbers and different bases. Let's end this post by converting from and to hexadecimal in C# and JavaScript. Because it's easy for computers to work with 1s and 0s, but they get too *long* for us humans. So sometimes it makes sense to use the base-16 number system, *hexadecimal*:

`0 = 0 1 = 1 2 = 2 3 = 3 4 = 4 5 = 5 6 = 6 7 = 7 8 = 8 9 = 9 A = 10 B = 11 C = 12 D = 13 E = 14 F = 15`

In JavaScript, it's easy to convert decimal to hexadecimal. Just do the following with `toString(radix)`

, and you'll get a string back:

`(15).toString(16); // "f" (101).toString(16); // "65" (1000).toString(16); // "3e8"`

Then to convert from hexadecimal to decimal, you can prefix the value with `0x`

. Else, if you have a string, then you can use `parseInt(string, radix)`

instead:

`0xf; // 15 0x65; // 101 0x3e8; // 1000 parseInt('f', 16); // 15 parseInt('65', 16); // 101 parseInt('3e8', 16); // 1000`

In C#, there are many different methods available, so we'll use one of the many approaches. For example, the System.Convert class *"converts a base data type to another base data type"* and provides a static `ToString`

method. We can also use `Int32.ToString(string)`

.

`Convert.ToString(15, 16); // f Convert.ToString(101, 16); // 65 Convert.ToString(1000, 16); // 3e8 15.ToString("x"); // f 101.ToString("x"); // 65 1000.ToString("x"); // 3e8`

C# also has a *parse* method:

`0xf; // 15 0x65; // 101 0x3e8; // 1000 Convert.ToInt32("f", 16); // 15 Convert.ToInt32("65", 16); // 101 Convert.ToInt32("3e8", 16); // 1000`