JS fractions for big numbers with no precision lose

347 Views Asked by At

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

There are 2 best solutions below

1
tevemadar On

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.

0
trincot On

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);