Introduction

JazzTeam is always happy to share its experience, in particular, in the field of front-end development. This article is the result of our developers’ work on several “heavyweight” projects of our partners. Each of these projects is somehow related to client-side mathematical/financial calculations (JavaScript). The article is not an academic paper, but it can help in solving practical problems encountered when implementing real projects and performing real tasks.

In software development, there are many scenarios where it is necessary to operate with huge numbers that go beyond the usual integer or fractional values. In this article, we will dive into working with large numbers and consider how JavaScript helps work with them effectively. We will get acquainted with the types of data provided by JavaScript for storing and manipulating large numbers. Next, we will proceed to a deeper study of the topic, and also consider manipulations with bits and their role in operations with large numbers. Then we will study arithmetic operations with large integers, explore methods and strategies that can be applied to perform the operations of addition, subtraction, multiplication and division with large numbers.

And finally, we’ll move on to large fractional numbers, consider the methods and algorithms that allow arithmetic operations to be carried out with high accuracy in decimal fractions.

1. Introduction to working with large numbers. Data types for working with numbers in JavaScript

In JavaScript, as in many other programming languages, there is a limit on the size of numbers, which is determined by internal limits on the representation of numbers in the floating-point binary system (IEEE 754). This limitation is due to the computer architecture and language standards.

Large numbers in JS usually refer to numbers that go beyond the maximum or minimum representation of floating-point numbers. Standard JavaScript has the Number data type that represents double-precision floating-point numbers. This data type has limitations on the accuracy and range of values.

Let’s study in detail what other data types exist for working with numbers in JS:

1. Number:

This data type represents both integers and decimal numbers, which are allocated 64 bits of memory. However, JavaScript uses a floating-point format (64-bit IEEE 754), and this can lead to some limitations when working with very large numbers. Number.MAX_VALUE and Number.MIN_VALUE (discussed below) represent the maximum and minimum values of the Number type.

In JavaScript, integers and real numbers are two basic types of numbers that are used to perform mathematical operations and store numerical data. Integers can be positive, negative, or zero. Real numbers can contain decimals and an exponent.

Code Example 1 – Examples of Integers and Real Numbers in JS:

Examples of Integers and Real Numbers in JS

2. BigInt:

This data type is used to work with very large integers of arbitrary length that cannot be accurately represented by the Number type. BigInt is useful when you need an accurate representation of large integers without losing accuracy, for example, when working with cryptography, large research calculations, or other tasks that require high accuracy. You can also perform arithmetic operations with Bigint like with ordinary numbers. However, BigInt and Number ordinary numbers cannot be mixed in the same arithmetic operations without an explicit transformation. It is also worth remembering that BigInt can have a high cost in terms of performance and memory, so its use should be limited only to those cases when it is really necessary.

Code Example 2 – BigInt Type Example in JS:

BigInt Type Example in JS

3. Special numerical values:

NaN (Not-a-Number): This is a special value that represents an error when performing mathematical operations if these operations cannot be performed. NaN typically occurs in the following situations:

  • Attempt to perform a mathematical operation or calculation that doesn’t make sense, such as dividing zero by zero or trying to extract a square root from a negative number.
  • Attempt to convert a non-numeric string to a number using the parseFloat or parseInt function.
  • Attempt to perform an arithmetic operation with a variable that has not been initialized.

Infinity and -Infinity: These values represent positive and negative infinity. They occur when, for example, a number is divisible by zero or a mathematical operation that exceeds the maximum value is performed.

So, it can be concluded that Infinity and -Infinity provide a way to represent infinity in numerical calculations and can be used to handle errors associated with division by zero and overflow.

4. Static constants:

Number.POSITIVE_INFINITY and Number.NEGATIVE_INFINITY: There are Number.POSITIVE_INFINITY and Number.NEGATIVE_INFINITY constants in JS
These constants can be used to address infinity values more reliably.

Number.MAX_VALUE and Number.MIN_VALUE: These static constants represent the maximum and minimum possible values of the Number type.

Number.MAX_VALUE: This constant represents the largest positive number that can be represented in JavaScript. It is approximately equal to 1.7976931348623157e+308. This number is the maximum possible value for double-precision floating-point numbers that are used in JavaScript. Numbers greater than MAX_VALUE will be represented as infinity.

Number.MIN_VALUE: This constant represents the smallest positive value that can be represented in JavaScript. It is approximately equal to 5e-324. This is the closest positive number to zero that can be used in JavaScript.
It should be noted that Number.MIN_VALUE represents the smallest positive value, but not a negative one. A negative value close to zero can be obtained by multiplying Number.MIN_VALUE by -1. Numbers smaller than 5e-324 are converted to 0.

Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER: These static constants represent the maximum and minimum possible integers that can be represented without loss of accuracy in JavaScript.

Number.MAX_SAFE_INTEGER: This constant represents the largest integer that can be represented in JavaScript without loss of accuracy. Its value is 9007199254740991. Any integer that goes beyond this limit may lose accuracy and will not be represented correctly.

Code Example 3 – Example of Loss of Accuracy when Going Beyond the Limits of the Maximum Safe Number in JS:

Example of Loss of Accuracy when Going Beyond the Limits of the Maximum Safe Number in JS

Number.MIN_SAFE_INTEGER: This constant represents the smallest integer that can be represented in JavaScript without loss of accuracy. Its value is -9007199254740991. Trying to represent a number smaller than Number.MIN_SAFE_INTEGER may also result in loss of accuracy.

These constants are useful when working with integers in JavaScript to avoid losing accuracy during mathematical operations with large integers.

2. Large numbers and manipulations with bits

Bit manipulations can be useful when working with large numbers, especially when you need to perform operations at the level of individual bits of numbers. To perform such manipulations with large numbers in JavaScript, you can use the following methods and techniques:

Bitwise AND (&): This operator executes bitwise AND between two numbers. The result is a number in which each bit is set to 1 only if the corresponding bits in both numbers are 1.

Code Example 4 – Example of Using Bitwise Operator AND (&):

Example of Using Bitwise Operator AND (&)

Bitwise OR (|): This operator executes bitwise OR between two numbers. The result is a number in which each bit is set to 1, if at least one of the corresponding bits in the numbers is 1.

Code Example 5 – Example of Using Bitwise OR (|):

Example of Using Bitwise OR (|)

Bitwise XOR (|): This operator executes bitwise XOR between two numbers. The result is a number in which each bit is set to 1, only if the corresponding bits in the numbers differ.

Code Example 6 – Example of Using Bitwise XOR (^):

Example of Using Bitwise XOR (^)

Bitwise negation (~): This operator inverts all bits of a number, i.e. changes 0 to 1 and 1 to 0. The result is a number in which all bits are inverted.

Code Example 7 – Example of Using Bitwise Negation (~):

Example of Using Bitwise Negation (~)

Bitwise left-shift (<<): This operator shifts the bits of a number to the left by a specified number of positions filling the vacant positions with zeros.

Code Example 8 – Example of Using Bitwise Left-Shift (<<):

Example of Using Bitwise Left-Shift (<<)

Bitwise right-shift (>>): This operator shifts the bits of a number to the right by a specified number of positions. The signed shift (with a sign) fills the vacant positions with copies of the high bit (to save the sign), and the unsigned shift ‒ with zeros.

Code Example 9 – Example of Using Bitwise Right-Shift (>>):

Example of Using Bitwise Left-Shift

Bit manipulations can be useful when dealing with large numbers in a number of scenarios. Here are some examples of these scenarios:

  • Data analysis and optimization: in data analysis and computation optimization tasks, bit operations can be used to perform certain calculations quickly and efficiently, such as counting bit units (popular in optimization tasks) or performing arithmetic operations with large numbers in JavaScript.
  • Implementation of cryptographic algorithms: cryptographic algorithms such as RSA or ECC require different bit operations when generating and processing large integers. Many cryptographic operations are based on complex bit masks, shifts, and other manipulations.
  • Hashing: many hashing algorithms, such as SHA-256, perform bitwise operations with large numbers to compute hash values. They can be used to verify data integrity and ensure uniqueness.
  • Graphical programming: in a number of graphical tasks, such as image creation and editing, pixel and color manipulation can be implemented using bitwise operations.

Let’s consider the optimization of arithmetic operations with large numbers using bit operations.

It is necessary to clarify that the use of bitwise operations can lead to loss of accuracy, since numbers in JavaScript are represented in a floating point format with limited accuracy. Therefore, this method is not suitable for all scenarios.

Let’s consider an example of bitwise addition of values of two large integers with bitwise shift of high bits, if necessary.

Code Example 10 – Example of Bitwise Addition of Values of Two Large Integers with Bitwise Shift of High Bits, if Necessary:

Example of Bitwise Addition of Values of Two Large Integers with Bitwise Shift of High Bits, if Necessary

3. Arithmetic operations with large integers

BigInt is usually used to work with very large integers beyond the Number type range. BigInt makes it possible to work with integers of arbitrary length.

You can create a BigInt number using the following methods:

  • Add ‘n’ to the number.

Code Example 11 – Example of Creating a BigInt Using the ‘n’ Character:

Example of Creating a BigInt Using the 'n' Character

  • Use the constructor for the string (it is better not to call the constructor for the number, as this can lead to loss of accuracy).

Code Example 12 – Example of Creating a BigInt with a Constructor Call for the String:

Example of Creating a BigInt with a Constructor Call for the String

Arithmetic operations with numbers of the BigInt type are performed similarly to operations with numbers of the Number type. However, it should be noted that both operands must be BigInt to perform arithmetic operations with BigInt The result will also be BigInt, and you can use the .toString() method to convert it to a string for output.

Also a key feature of BigInt is that the division operator for BigInt rounds the result down to an integer, because BigInt represents only integers.

Code Example 13 – Example of Rounding the Result after Dividing Two Integers Downwards:

Example of Rounding the Result after Dividing Two Integers Downwards

JavaScript does not provide a built-in method to perform BigInt division to obtain a floating-point decimal number. If you really want to do exact division with a decimal remainder, you’ll have to use a third-party library like Bignumber.js, which provides support for floating-point calculations for large numbers.

Let’s consider how arithmetic operations work in BigInt under the hood:

  • Addition is performed by bitwise adding the values of two numbers, shifting high bits bitwise, if necessary. (Addition is implemented according to a similar algorithm, as in “Code Example 10 – Example of Implementation of Bitwise Addition of Values of Two Large Integers with Bitwise Shift of High Bits, if Necessary”).
  • Subtraction is performed by bitwise subtracting the values of two numbers, shifting high bits bitwise, if necessary.
  • Multiplication is performed by multiplying the values of two numbers using the Karatsuba algorithm, which is one of the most efficient multiplication algorithms for large numbers. The Karatsuba algorithm works as follows: numbers are divided into several blocks of the same length, then each block is multiplied by the corresponding value from another number, and the results of multiplication are combined to obtain the final result.
  • Division is performed by dividing the values of two numbers using the Steiner-Wilson algorithm, which is one of the most efficient division algorithms for large numbers. The Steiner-Wilson algorithm works as follows: the number that is divided (divisible) and the number that is divided (divisor) are compared. If the divisor is greater than the divisible, then zero is written in the result. Otherwise, you need to start dividing. The leftmost digit of the divisible is divided by the divisor, and the quotient and the remainder are obtained. This will be the current result. The quotient will be one of the digits of the division result.
    The next digit from the divisible number is taken and appended to the remainder on the right to get a new number that will be a new divisible number. These steps are repeated until all the digits of the divisible are passed. When the division of all digits is completed, the result is obtained from the quotient digits and the remainder, which can be ignored. (See “Image 1 – Flowchart of the Steiner-Wilson Algorithm for Dividing Large Numbers“).
Flowchart of the Steiner-Wilson algorithm for dividing large numbers
Figure 1 - Flowchart of the Steiner-Wilson algorithm for dividing large numbers

These algorithms provide high accuracy and efficiency of calculations using BigInt. They allow performing arithmetic operations with large numbers quickly and accurately.

There is another problem that you may encounter when working with the BigInt type: the standard Math object does not work with numbers of the BigInt type. This is because BigInt is a new data type that was added to JavaScript in ES2020. The standard Math was developed before the advent of BigInt, and therefore it was not adapted to work with this type of data. To use the methods of the Math object with numbers of BigInt type, you need to use a library that provides support for this data type. For example, you can use the previously mentioned Bignumber.js library. Without a library, it will be necessary to implement specialized functions that require analysis to select the best algorithm for performing various mathematical operations. At the same time, it is also necessary to take into account possible errors and apply a data caching mechanism. All this is a rather time-consuming process, so it is better to focus on the use of libraries.

When looking for possible libraries to work with large numbers in JS, you will come across libraries such as Bignumber.js and Decimal.js. These libraries are designed to work with large numbers in JavaScript. They have a number of common features, but they also have some key differences. In the Decimal library, there are limitations on the number of decimal places and the total length of the number, which can be represented with maximum accuracy. If the number exceeds these limits, then Decimal will store it with truncated accuracy, which can lead to loss of significant digits.

Code Example 14 – Example of Accuracy Loss when Adding Two Large Numbers Using the Decimal.js library:

Example of Accuracy Loss when Adding Two Large Numbers Using the Decimal.js library

Accordingly, based on the presented result, the Decimal.js library is not suitable for working with very large numbers, because you can cope with such a task even just using the BigInt type.

Code Example 15 – Example of Correctly Calculating the Sum of Two Large Numbers Using BigInt:

Example of Correctly Calculating the Sum of Two Large Numbers Using BigInt

The Bignumber.js library prevents rounding errors and provides an accurate representation of large numbers, making it suitable for tasks requiring high accuracy. Also, Bignumber.js provides a wide range of operations with large numbers, including mathematical functions and operations with different number systems. The library is highly flexible and can be used in a variety of mathematical problems.

Code Example 16 – Example of Using the Built-In Function of the Bignumber.js Library to Calculate the Root of a Number:

Example of Using the Built-In Function of the Bignumber.js Library to Calculate the Root of a Number

The results are presented in Table 1 – “Recommendations for Selection”. There you will know when it is better to use BigInt, and when ‒ the Bignumber.js library.

Table 1 – Recommendations for Selection:

Recommendations for Selection

4. Arithmetic operations with large fractional numbers

JavaScript can work with fractional numbers of different sizes. However, when working with very large fractional numbers that go beyond the standard floating-point numerical representation, accuracy limitations may occur. This may lead to loss of accuracy when performing arithmetic operations.

Since BigInt only works with integer values, you can use string representation of numbers and perform arithmetic operations manually to work with very large fractional numbers in JavaScript, without using third-party libraries. However, manual calculation with large numbers may require more complex code to control accuracy, as well as considering bit transitions and handling fractional parts.

Let’s consider an example of addition operation implementation:

Code Example 17 – Example of Manual Implementation of the Operation of Adding Two Large Fractional Numbers:

Example of Manual Implementation of the Operation of Adding Two Large Fractional Numbers

In order not to perform operations manually, the best option would be to use the Bignumber.js library discussed earlier. This library provides tools for working with large numbers while maintaining high accuracy. The library can be more convenient and secure, especially if you need to perform complex calculations or process a large number of operations.

This is what the code for adding two large fractional numbers might look like when using the Bignumber.js library.

Code Example 18 – Example of Implementing an Addition Operation Using the Bignumber.js Library:

Example of Implementing an Addition Operation Using the Bignumber.js Library

For performance comparison, identical very large numbers with many decimal places were taken. The results of code execution time measurements using console.time are presented below.

Result of Measuring the Code Execution Time when Manually Implementing the Operation of Adding Two Large Fractional Numbers
Figure 2 - Result of Measuring the Code Execution Time when Manually Implementing the Operation of Adding Two Large Fractional Numbers
Result of Measuring the Code Execution Time when Implementing the Operation of Adding Two Large Fractional Numbers Using the Bignumber.js library
Figure 3 - Result of Measuring the Code Execution Time when Implementing the Operation of Adding Two Large Fractional Numbers Using the Bignumber.js library

Based on the results of measuring the code execution time, it can be seen that using the Bignumber.js library to perform arithmetic operations with large fractional numbers reduces the code execution time. In this case, the Bignumber.js library demonstrated the acceleration by more than twice compared to manual calculation.

This confirms that libraries optimized for working with large numbers usually provide more efficient and faster results than manual calculation, especially when performing complex arithmetic operations.

Conclusion

In this article, we studied the data types provided by JavaScript for working with numbers and made sure that the language has the tools to work with large numbers. We considered manipulations with bits and their use in operations with large numbers, and gave an example of the implementation of bitwise addition of values of two large integers. We considered arithmetic operations with large integers and gave recommendations regarding the use of BigInt and the Bignumber.js library. We also considered how to work with large fractional numbers and made arguments in favor of using the Bignumber.js library in this situation.

Understanding the concepts of working with large numbers, which were discussed above, can be useful in the development of high-performance and accurate applications, including mathematical calculations, cryptography, processing large amounts of data, and many other areas.