skip to Main Content

Big numbers with fraction support:

I want to perform math operation with no precision loss.
How can I do it using JavaScript?
First I tried decimal.js, but it does not preserve precision as you see in next example:

import {Decimal} from 'decimal.js';
const a1 = new Decimal(1);
const a2 = new Decimal(3);
console.log(a1.div(a2).toFraction()); // should be `1/3` but I get `33333333333333333333/100000000000000000000`

Next I tried fraction.js but it does not work with large numbers, for example:

import Fraction from 'fraction.js';
const n1 = new Fraction("99999999999999999999999999999999999999999999999");
const n2 = new Fraction("99999999999999999999999999999999999999999999990");
console.log(+(n1.sub(n2))) // should be 9, but I get 0

Are there any solutions to work with relative large numbers(lets say same as decimal.js supports) but with high precision(same as fraction.js supports).

I checked out that mathjs uses fraction.js under the hood, so no any advantages for large numbers:

import * as math from "mathjs";
const a1 = math.fraction(math.number("99999999999999999999999999999999999999999999999"))
const a2 = math.fraction(math.number("99999999999999999999999999999999999999999999990"))
console.log(+(math.subtract(a1, a2))) // should be 9 but I get 0

2

Answers


  1. That’s more like a bug you found in decimal.js. If you manually provide the maximum denominator argument, it produces 1/3.

    const a1 = new Decimal(1);
    const a2 = new Decimal(3);
    console.log(a1.div(a2).toFraction(-1>>>1)); // maxD = largest 32-bit signed int
    <script src="https://cdn.jsdelivr.net/gh/MikeMcl/decimal.js/decimal.js"></script>

    https://github.com/MikeMcl/decimal.js/issues seems to be where this could be reported.

    Login or Signup to reply.
  2. Given that JavaScript has a BigInt data type, it is not so hard to implement the basic arithmetic operations yourself. Most logic will be needed in the constructor so to normalise the fraction (denominator should not be negative, numerator and denominator should be coprime) and deal with type conversion.

    For example:

    class BigFraction {
        #n
        #d
        
        constructor(n, d=1n) {
            const abs = a => a < 0n ? -a : a;
            const gcd = (a, b) => b ? gcd(b, a % b) : a;
            const sign = a => a < 0n ? -1n : 1n;
    
            // Pass-through
            if (n instanceof BigFraction) return n;
            if (typeof d !== "bigint") throw "Second argument should be bigint when provided";
            if (typeof n !== "bigint") {
                if (arguments.length != 1) throw "Only one argument allowed when first is not a bigint"
                const s = String(n);
                const match = s.match(/^([+-]?d+)(?:([./])(d+))?$/)
                if (!match) throw `'${s}' not recognised as number`
                if (match[2] == "/") {
                    n = BigInt(match[1]);
                    d = BigInt(match[3]);
                } else {
                    n = BigInt(s.replace(".", ""));
                    d = BigInt("1".padEnd((match[3]?.length ?? 0) + 1, "0"));
                }
            }
            // Normalize
            const factor = sign(n) * sign(d);
            n = abs(n)
            d = abs(d)
            const common = gcd(n, d);
            this.#n = factor * n / common;
            this.#d = d / common;
        }
        add(num) {
            const other = new BigFraction(num);
            return new BigFraction(this.#n * other.#d + other.#n * this.#d, this.#d * other.#d)
        }
        subtract(num) {
            const other = new BigFraction(num);
            return new BigFraction(this.#n * other.#d - other.#n * this.#d, this.#d * other.#d)
        }
        multiply(num) {
            const other = new BigFraction(num);
            return new BigFraction(this.#n * other.#n, this.#d * other.#d)
        }
        divide(num) {
            const other = new BigFraction(num);
            return new BigFraction(this.#n * other.#d, this.#d * other.#n)
        }
        inverse() {
            return new BigFraction(this.#d, this.#n);
        }
        sign() {
            return this.#n < 0n ? -1 : this.#n > 0n ? 1 : 0; 
        }
        negate() {
            return new BigFraction(-this.#n, this.#d);
        }
        compare(num) {
            return this.subtract(num).sign();        
        }
        toString() {
            const s = this.#n.toString();
            return this.#d != 1n ? s + "/" + this.#d : s;
        }
        valueOf() { // Conversion to number data type (float precision)
            return Number(this.#n) / Number(this.#d);
        }    
    }
    
    // Demo
    var a = new BigFraction("12.34");
    console.log("12.34 is " + a);
    var b = a.add("-36/5");
    console.log("12.34 - 36/5 is " + b);
    var c = b.multiply("99128362832913233/9182");
    console.log("(12.34 - 36/5) * (99128362832913233/9182) is " + c);
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search