Source: types/big-decimal.js

"use strict";
const Integer = require("./integer");
const utils = require("../utils");

/** @module types */

/**
 * The `BigDecimal` class provides operations for
 * arithmetic, scale manipulation, rounding, comparison and
 * format conversion. The {@link #toString} method provides a
 * canonical representation of a `BigDecimal`.
 */
class BigDecimal {
    /**
     * Constructs an immutable arbitrary-precision signed decimal number.
     * A `BigDecimal` consists of an [arbitrary precision integer]{@link module:types~Integer}
     * *unscaled value* and a 32-bit integer *scale*. If zero
     * or positive, the scale is the number of digits to the right of the
     * decimal point. If negative, the unscaled value of the number is
     * multiplied by ten to the power of the negation of the scale. The
     * value of the number represented by the `BigDecimal` is
     * therefore <tt>(unscaledValue &times; 10<sup>-scale</sup>)</tt>.
     *  * A `BigDecimal` consists of an arbitrary precision integer
     * *unscaled value* and a 32-bit integer *scale*. If zero
     * or positive, the scale is the number of digits to the right of the
     * decimal point. If negative, the unscaled value of the number is
     * multiplied by ten to the power of the negation of the scale. The
     * value of the number represented by the `BigDecimal` is
     * therefore (unscaledValue × 10^(-scale)).
     *
     * @param {Integer|Number} unscaledValue The integer part of the decimal.
     * @param {Number} scale The scale of the decimal.
     */
    constructor(unscaledValue, scale) {
        if (typeof unscaledValue === "number") {
            unscaledValue = Integer.fromNumber(unscaledValue);
        }
        /**
         * @type {Integer}
         * @private
         */
        this._intVal = unscaledValue;
        /**
         * @type {Number}
         * @private
         */
        this._scale = scale;
    }

    /**
     * Returns the BigDecimal representation of a buffer composed of the scale (int32BE) and the unsigned value (varint BE)
     * @param {Buffer} buf
     * @returns {BigDecimal}
     */
    static fromBuffer(buf) {
        const scale = buf.readInt32BE(0);
        const unscaledValue = Integer.fromBuffer(buf.slice(4));
        return new BigDecimal(unscaledValue, scale);
    }

    /**
     * Returns a buffer representation composed of the scale as a BE int 32 and the unsigned value as a BE varint
     * @param {BigDecimal} value
     * @returns {Buffer}
     */
    static toBuffer(value) {
        const unscaledValueBuffer = Integer.toBuffer(value._intVal);
        const scaleBuffer = utils.allocBufferUnsafe(4);
        scaleBuffer.writeInt32BE(value._scale, 0);
        return Buffer.concat(
            [scaleBuffer, unscaledValueBuffer],
            scaleBuffer.length + unscaledValueBuffer.length,
        );
    }

    /**
     * Returns a BigDecimal representation of the string
     * @param {String} value
     * @returns {BigDecimal}
     */
    static fromString(value) {
        if (!value) {
            throw new TypeError("Invalid null or undefined value");
        }
        value = value.trim();
        const scaleIndex = value.indexOf(".");
        let scale = 0;
        if (scaleIndex >= 0) {
            scale = value.length - 1 - scaleIndex;
            value = value.substr(0, scaleIndex) + value.substr(scaleIndex + 1);
        }
        return new BigDecimal(Integer.fromString(value), scale);
    }

    /**
     * Returns a BigDecimal representation of the Number
     * @param {Number} value
     * @returns {BigDecimal}
     */
    static fromNumber(value) {
        if (isNaN(value)) {
            return new BigDecimal(Integer.ZERO, 0);
        }
        let textValue = value.toString();
        if (textValue.indexOf("e") >= 0) {
            // get until scale 20
            textValue = value.toFixed(20);
        }
        return BigDecimal.fromString(textValue);
    }

    /**
     * Returns true if the value of the BigDecimal instance and other are the same
     * @param {BigDecimal} other
     * @returns {Boolean}
     */
    equals(other) {
        return other instanceof BigDecimal && this.compare(other) === 0;
    }

    inspect() {
        return this.constructor.name + ": " + this.toString();
    }

    /**
     * @param {BigDecimal} other
     * @returns {boolean}
     */
    notEquals(other) {
        return !this.equals(other);
    }

    /**
     * Compares this BigDecimal with the given one.
     * @param {BigDecimal} other Integer to compare against.
     * @return {number} 0 if they are the same, 1 if the this is greater, and -1
     *     if the given one is greater.
     */
    compare(other) {
        const diff = this.subtract(other);
        if (diff.isNegative()) {
            return -1;
        }
        if (diff.isZero()) {
            return 0;
        }
        return +1;
    }

    /**
     * Returns the difference of this and the given BigDecimal.
     * @param {BigDecimal} other The BigDecimal to subtract from this.
     * @return {!BigDecimal} The BigDecimal result.
     */
    subtract(other) {
        const first = this;
        if (first._scale === other._scale) {
            return new BigDecimal(
                first._intVal.subtract(other._intVal),
                first._scale,
            );
        }
        let diffScale;
        let unscaledValue;
        if (first._scale < other._scale) {
            // The scale of this is lower
            diffScale = other._scale - first._scale;
            // multiple this unScaledValue to compare in the same scale
            unscaledValue = first._intVal
                .multiply(Integer.fromNumber(Math.pow(10, diffScale)))
                .subtract(other._intVal);
            return new BigDecimal(unscaledValue, other._scale);
        }
        // The scale of this is higher
        diffScale = first._scale - other._scale;
        // multiple this unScaledValue to compare in the same scale
        unscaledValue = first._intVal.subtract(
            other._intVal.multiply(Integer.fromNumber(Math.pow(10, diffScale))),
        );
        return new BigDecimal(unscaledValue, first._scale);
    }

    /**
     * Returns the sum of this and the given <code>BigDecimal</code>.
     * @param {BigDecimal} other The BigDecimal to sum to this.
     * @return {!BigDecimal} The BigDecimal result.
     */
    add(other) {
        const first = this;
        if (first._scale === other._scale) {
            return new BigDecimal(
                first._intVal.add(other._intVal),
                first._scale,
            );
        }
        let diffScale;
        let unscaledValue;
        if (first._scale < other._scale) {
            // The scale of this is lower
            diffScale = other._scale - first._scale;
            // multiple this unScaledValue to compare in the same scale
            unscaledValue = first._intVal
                .multiply(Integer.fromNumber(Math.pow(10, diffScale)))
                .add(other._intVal);
            return new BigDecimal(unscaledValue, other._scale);
        }
        // The scale of this is higher
        diffScale = first._scale - other._scale;
        // multiple this unScaledValue to compare in the same scale
        unscaledValue = first._intVal.add(
            other._intVal.multiply(Integer.fromNumber(Math.pow(10, diffScale))),
        );
        return new BigDecimal(unscaledValue, first._scale);
    }

    /**
     * Returns true if the current instance is greater than the other
     * @param {BigDecimal} other
     * @returns {boolean}
     */
    greaterThan(other) {
        return this.compare(other) === 1;
    }

    /** @return {boolean} Whether this value is negative. */
    isNegative() {
        return this._intVal.isNegative();
    }

    /** @return {boolean} Whether this value is zero. */
    isZero() {
        return this._intVal.isZero();
    }

    /**
     * Returns the string representation of this <code>BigDecimal</code>
     * @returns {string}
     */
    toString() {
        let intString = this._intVal.toString();
        if (this._scale === 0) {
            return intString;
        }
        let signSymbol = "";
        if (intString.charAt(0) === "-") {
            signSymbol = "-";
            intString = intString.substr(1);
        }
        let separatorIndex = intString.length - this._scale;
        if (separatorIndex <= 0) {
            // add zeros at the beginning, plus an additional zero
            intString =
                utils.stringRepeat("0", -separatorIndex + 1) + intString;
            separatorIndex = intString.length - this._scale;
        }
        return (
            signSymbol +
            intString.substr(0, separatorIndex) +
            "." +
            intString.substr(separatorIndex)
        );
    }

    /**
     * Returns a Number representation of this `BigDecimal`.
     * @returns {Number}
     */
    toNumber() {
        return parseFloat(this.toString());
    }

    /**
     * Returns the string representation.
     * Method used by the native JSON.stringify() to serialize this instance.
     */
    toJSON() {
        return this.toString();
    }
}

module.exports = BigDecimal;