An overview of the

Decimals and large numbers are two data types that give us a bit of a headache when it comes to common JavaScript number manipulation.

  • In large number operations, we often encounter out-of-range situations because of the number length limitation of type number. For example, in the case of passing Long data, we can only convert it to a string for passing and processing.
  • And in the process of decimal number operation, JavaScript due to its data representation, resulting in the decimal operation will be inaccurate. The classic example is 0.3-0.2, which is not equal to 0.1, but 0.09999999999999998.

In a previous blog post I introduced a library for Long data processing called Long.js, which can handle contorted data more efficiently. To extend JavaScript in a data type processing capabilities, if you are interested in you can go to see this article: long.js source code analysis and learning.

The library we need to introduce today, which can handle not only Long data, but also decimals accurately, is big.js.

The library also has a long history, with the first commit recorded in 2012. It now has 3.2k stars, which is an indication of how popular the library has become. At the same time, this library has a complete unit test, so now all the issues have been solved, so we don’t have to worry about the quality of this library.

Code sample

First of all, let’s take a look at how the big.js library is used, and what specific application scenarios and functions it has.

x = new Big(123.4567)
y = Big('123456.7 e-3')                 // 'new' is optional
z = new Big(x)
x.eq(y) && x.eq(z) && y.eq(z)


0.3 - 0.1                              / / 0.19999999999999998
x = new Big(0.3)
x.minus(0.1)                           / / "0.2"
x                                      / / "0.3"
Copy the code

From the code, we can see that all big.js operations are based on the Big class. The Big class implements some of the common operations we do with numbers, such as addition, subtraction, multiplication, and division, and comparison. Basically all the operations you use should be supported.

To learn exactly what big.js supports, read the big.js API documentation.

Introduction of the API

Big.js API is divided into the following two parts:

  • Constants defined
  • Operational function

Now, let’s look at one piece at a time.

Constants defined

There are five constant definitions of big.js.

  • DP, number of decimal places, default is 20
  • RM: round to the nearest integer. The default value is 1. If it is 0.5, then round down.
  • NE: The smallest decimal number displayed as scientific notation when converted to a string. The default value is -7, which is not 0 until the seventh decimal point.
  • PE: Displays the smallest integer number of bits in bitscientific notation when converted to a string. The default value is 21, that is, the number contains more than 21 digits.
  • Strict: The default value is false. When set to true, the constructor accepts only strings and large numbers.

Operators operate on functions

  • Abs, take the absolute value.
  • CMP, short for compare.
  • Div, division.
  • Eq, short for equal, equal comparison.
  • Gt is greater than.
  • Gte is less than or equal to e equals.
  • Lt is less than.
  • Lte, less than or equal to, e stands for equal.
  • Minus C.
  • Mod, mod.
  • Plus, addition.
  • Pow to the power.
  • Prec, rounded by precision, with the argument representing the global number of digits.
  • Round, rounded by precision, the argument represents the number of decimal places.
  • SQRT, take the square root.
  • Times, times.
  • ToExponential, which translates to scientific counting, where the parameters represent the number of places of accuracy.
  • ToFied, complete digit, parameter stands for decimal number.
  • ToJSON and toString, converted to strings.
  • ToPrecision, display by specified significant digit, argument is significant digit.
  • ToNumber, which is converted to the JavaScript number type.
  • ValueOf, a string containing a negative sign (if negative or -0).

The source code parsing

Big.js source code content is relatively small, all in a big.js file, if you want to read, directly look at the file. Let’s take a look at the code structure of big.js.

We won’t go into constant definitions, there’s nothing complicated about this code. We’ll focus on how internal data is stored after initialization, and we’ll pick a few specific apis to see how they’re implemented.

Variables store

Let’s look at constructors first.

function Big(n) {
    var x = this;

    // Support function call initialization without using the new operator
    if(! (xinstanceof Big)) return n === UNDEFINED ? _Big_() : new Big(n);

    // The prototype chain checks to see if the incoming value is already an instance of the Big class
    if (n instanceof Big) {
        x.s = n.s;
        x.e = n.e;
        x.c = n.c.slice();
    } else {
        if (typeofn ! = ='string') {
            if (Big.strict === true) {
                throw TypeError(INVALID + 'number');
            }

            // Determine if it is -0, if not, convert it to a string.
            n = n === 0 && 1 / n < 0 ? '0' : String(n);
        }

        // The parse function accepts only string arguments
        parse(x, n);
    }

    x.constructor = Big;
}
Copy the code

In the constructor, the variable n passed in is typed into a string that is passed to the parse function. So, let’s look at what the parse function does.

function parse(x, n) {
    var e, i, nl;

    if(! NUMERIC.test(n)) {throw Error(INVALID + 'number');
    }

    // Check whether the symbol is positive or negative
    x.s = n.charAt(0) = =The '-' ? (n = n.slice(1), -1) : 1;

    // Check if there is a decimal point
    if ((e = n.indexOf('. '> -))1) n = n.replace('. '.' ');

    // Determine if it is a scientific notation
    if ((i = n.search(/e/i)) > 0) {

        // Determine the index value
        if (e < 0) e = i;
        e += +n.slice(i + 1);
        n = n.substring(0, i);
    } else if (e < 0) {

        // is a positive integer
        e = n.length;
    }

    nl = n.length;

    // Determine if the number is preceded by a 0, such as 0123
    for (i = 0; i < nl && n.charAt(i) == '0';) ++i;

    if (i == nl) {

        // Zero.
        x.c = [x.e = 0];
    } else {

        // Identify the 0 after the number, such as 1.230
        for (; nl > 0 && n.charAt(--nl) == '0';) ; x.e = e - i -1;
        x.c = [];

        // The string is converted into an array for storage, with the preceding and following zeros removed
        for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++);
    }

    return x;
}
Copy the code

In the parse function, the data is parsed. First of all, we judge whether it meets the standard of numbers. If so, we judge whether the number method of the incoming data representation is negative number, decimal number and scientific counting method. At the same time, we deal with some meaningless 0.

With the parse function, we have converted the data passed in by the constructor into properties in the Big class instance. Where, variables stored in the Big class instance have the following meanings:

  • S, for symbol,- 1Minus,1That’s a positive number.
  • C is an array that stores the value of each bit of the current number.
  • E, represents the beginning of the decimal, that is, the number of elements in the array is the beginning of the decimal. For example in [1,2,3,4], ifeIt’s 2, so it’s 12.34.

From the above storage structure description, you should have a clear understanding of how big.js data is stored. And you should be able to get a sense of what the next operations are going to do with this data. Next, let’s pick a few of the most common functions to see if we’re right.

API source code parsing

Because big.js supports more operations, so we will choose a few more representative, others you are interested in, you can follow the source code to see, overall or very good understanding.

add

First of all, let’s look at the simplest of the four operations, addition, which is very simple to think about, just need to judge the sign, corresponding to the number of the addition and subtraction operations can be. Let’s look at the implementation code.

P.plus = P.add = function (y) {
    var t,
        x = this,
        Big = x.constructor,
        a = x.s,
        // All operations are converted into two instances of the Big class for easy processing
        b = (y = new Big(y)).s;

    // Check whether the symbols are not equal, i.e. one is positive and one is negative
    if(a ! = b) { y.s = -b;return x.minus(y);
    }

    var xe = x.e,
        xc = x.c,
        ye = y.e,
        yc = y.c;

    // Determine if a value is 0
    if(! xc[0) | |! yc[0]) return yc[0]? y :new Big(xc[0]? x : a *0);

    // Make a copy of the group to avoid affecting the original instance
    xc = xc.slice();

    // Fill in 0 to ensure the same number of digits
    // Note that the reverse function is faster than the unshift function
    if (a = xe - ye) {
        if (a > 0) {
            ye = xe;
            t = yc;
        } else {
            a = -a;
            t = xc;
        }

        t.reverse();
        for (; a--;) t.push(0);
        t.reverse();
    }

    // Place xC in a longer array for subsequent loop addition operations
    if (xc.length - yc.length < 0) {
        t = yc;
        yc = xc;
        xc = t;
    }

    a = yc.length;

    // Perform the addition operation to save the values to XC
    for (b = 0; a; xc[a] %= 10) b = (xc[--a] = xc[a] + yc[a] + b) / 10 | 0;

    // No need to check 0, because +x + +y! Is equal to 0, and then minus x plus minus y factorial. = 0

    if (b) {
        xc.unshift(b);
        ++ye;
    }

    // Delete the trailing 0
    for (a = xc.length; xc[--a] === 0;) xc.pop();

    y.c = xc;
    y.e = ye;

    return y;
};
Copy the code

From the above code, we can see that the addition operation is actually in the case of the same symbol, align the two digits of the decimal point, and then add each pair of data in the array, get the result and then save. This is a relatively simple operation.

In the code, big.js ensures the simplicity of the calculation process through some boundary condition judgment and exchange methods.

The multiplication

Now that we’ve done simple addition, let’s do slightly more complicated multiplication. In fact, the essence of multiplication and addition are similar, each digit after operation and then save back to the original array. It’s easy to understand this code if you think about the way you did multiplication in elementary school.

P.times = P.mul = function (y) {
    var c,
        x = this,
        Big = x.constructor,
        xc = x.c,
        yc = (y = new Big(y)).c,
        a = xc.length,
        b = yc.length,
        i = x.e,
        j = y.e;

    // symbol comparison determines whether the final sign is positive or negative
    y.s = x.s == y.s ? 1 : -1;

    // If one of the values is 0, return 0
    if(! xc[0) | |! yc[0]) return new Big(y.s * 0);

    // The decimal point is initialized to x.e+ Y.e. This is how we calculate the decimal point when multiplying two decimals
    y.e = i + j;

    // This step also ensures that the length of xc is never less than the length of yc, since xc is traversed to perform the operation
    if (a < b) {
        c = xc;
        xc = yc;
        yc = c;
        j = a;
        a = b;
        b = j;
    }

    // Initialize the result array with 0
    for (c = new Array(j = a + b); j--;) c[j] = 0;

    // I is initialized to the length of xc
    for (i = b; i--;) {
        b = 0;

        // A is the length of yc
        for (j = a + i; j > i;) {

            // Multiply one bit of xc by one bit of yc to get the final value, save it
            b = c[j] + yc[i] * xc[j - i - 1] + b;
            c[j--] = b % 10;

            b = b / 10 | 0;
        }

        c[j] = b;
    }

    // If there is a carry, then adjust the number of decimal places (increase y.e), otherwise delete the first 0
    if (b) ++y.e;
    else c.shift();

    // delete the following 0
    for(i = c.length; ! c[--i];) c.pop(); y.c = c;return y;
};
Copy the code

And the whole idea of multiplication is similar, you determine the sign, you adjust the number of decimal places, and then you multiply to get the final result. The overall code still looks clean.

integer

Having looked at the four operations represented by addition and multiplication, let’s look at the rounding operation.

In big.js, all round operations call an internal round function. So, let’s take the round method in the API as an example. This method takes two parameters. The first value dp represents the number of digits that are valid after decimals, and the second rm represents the way to round.

P.round = function (dp, rm) {
    if (dp === UNDEFINED) dp = 0;
    else if(dp ! == ~~dp || dp < -MAX_DP || dp > MAX_DP) {throw Error(INVALID_DP);
    }
    return round(new this.constructor(this), dp + this.e + 1, rm);
};

function round(x, sd, rm, more) {
    var xc = x.c;

    if (rm === UNDEFINED) rm = Big.RM;
    if(rm ! = =0&& rm ! = =1&& rm ! = =2&& rm ! = =3) {
        throw Error(INVALID_RM);
    }

    if (sd < 1) {
        // In the case of bottom pockets, the precision is less than 1, and the default valid value is 1
        more =
            rm === 3&& (more || !! xc[0]) || sd === 0 && (
                rm === 1 && xc[0] > =5 ||
                rm === 2 && (xc[0] > 5 || xc[0= = =5 && (more || xc[1] !== UNDEFINED))
            );

        xc.length = 1;

        if (more) {

            // 1, 0.1, 0.01, 0.001, 0.0001, etc
            x.e = x.e - sd + 1;
            xc[0] = 1;
        } else {
            // Define 0
            xc[0] = x.e = 0; }}else if (sd < xc.length) {

        // In the xC array, the paper after the precision is discarded and rounded
        more =
            rm === 1 && xc[sd] >= 5 ||
            rm === 2 && (xc[sd] > 5 || xc[sd] === 5 &&
                (more || xc[sd + 1] !== UNDEFINED || xc[sd - 1] & 1)) ||
            rm === 3&& (more || !! xc[0]);

        // Delete the array value with the desired precision
        xc.length = sd--;

        // Determine the integer
        if (more) {

            // Rounding may mean that the previous number must be rounded, so 0 is required
            for (; ++xc[sd] > 9;) {
                xc[sd] = 0;
                if(! sd--) { ++x.e; xc.unshift(1); }}}// Delete the 0 after the decimal point
        for(sd = xc.length; ! xc[--sd];) xc.pop(); }return x;
}
Copy the code

From the implementation of the internal round function, we can see that at the very beginning, we carried out the exception of the bottom of the pocket detection, excluding two abnormal cases. One is a parameter error, directly throw an exception; The other is the case where the precision is less than 1, where the bottom of the pocket is defined as 1.

In normal logic, we abandon the value after the precision according to the precision, and uniformly fill 0 for representation.

Source code analysis summary

In the big.js source code, we can see how large numbers are handled by breaking them up into bits and then performing each bit operation to get the result.

There are other operations that we haven’t talked about, such as absolute value, subtraction, division, power, etc., which are very simple. They all get the final result by manipulating the string of three properties we store in the instance: symbols, decimal places, and numbers. This is due to space reasons, we will not repeat, you can go to see the source code.

Let’s take a look at some of the good things we can learn from the source code:

  • Big numbers. Reading the source code can make us more clear about the number of operations.
  • The processing sequence is unified. In each operation function, we first detect anomalies and then process the data. Finally, we define a unified processing logic to perform operations on the data. If we encounter a value that does not conform to this processing logic, we are all converted to the correct value before processing. This way, our code looks clear and unambiguous, and we don’t need to process different types of data through different logic processing code.

However, we found some minor flaws in the code, such as the use of numbers for constant definitions rather than constants or strings, which are easier to understand. This could be optimized.

conclusion

Overall, big.js is a very lean library. Its source code is relatively easy to understand. This approach is much simpler and looks much more straightforward than the previous long.js approach.

The data type BigInt is also supported in the latest JavaScript. This will cover some of the operations and processing that occur when an integer exceeds the Number type, if you’re interested.

In general, I still recommend you to use a library like big.js to process large numbers. One is to ensure the compatibility of various platforms, and there is no problem of cross-platform and high and low container versions. The other is to unify the numeric data type, which is convenient for subsequent unified processing (BigInt and Number types cannot be used together. BigInt does not support methods in Math objects.

If you need to perform operations on large numbers later, you can consider using this simplified and convenient library.

If you want to see the code with Chinese annotations, you can also go to my GitHub to see my folk code, which has part of Chinese, and will be completed later.