0

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
valerii15298
  • 737
  • 4
  • 15
  • 1
    You could write your own library for this. It's pretty simple. – jabaa May 27 '23 at 11:55
  • 1
    @jabaa are you joking? decimal.js, fraction.js or mathjs are quite big libraries with lot of features... Writing such library for sure is not a simple task... And anyway it is better to have such library in the community than for every team to develop it separately – valerii15298 May 27 '23 at 11:59
  • You want rational numbers based on BigInt or some other implementation of arbitrary-length integers. – blakkwater May 27 '23 at 12:08
  • @blakkwater yes... but seems there is no js library that supports this... – valerii15298 May 27 '23 at 12:10
  • That first result looks like a bug (which I can replicate), since the docs say the first number is meant to be the numerator and the second the denominator. You might consider reporting it. (And perhaps fixing it. :-) ) – T.J. Crowder May 27 '23 at 12:11
  • @valerii15298 [BigRational.js](https://github.com/peterolson/BigRational.js/) – first thing from Google for "js rational numbers bigint". But I don't know, it's just a random suggestion ¯\\_(ツ)_/¯ – blakkwater May 27 '23 at 12:15
  • It depends on your requirements. Writing code for the requirements in this question would take less than 2 hours. I wrote a similar library in my studies with much more features in some days. – jabaa May 27 '23 at 12:15
  • @blakkwater thank you, but that library doe not seem to weel maintained with last commit on `Jan 15, 2019` – valerii15298 May 27 '23 at 12:23
  • @valerii15298 In Fraction.js, there's [bigfraction.js](https://github.com/infusion/Fraction.js/blob/master/bigfraction.js) which uses BigInt. Have you tried it? – blakkwater May 27 '23 at 12:37
  • 3
    Your question is either too broad, if you you're asking, how to write the code, or it asks for library recommendations, which is off-topic. If you have a question about one specific library, you could ask it here or on the GitHub page. Currently, you're asking about multiple libraries. – jabaa May 27 '23 at 12:39

2 Answers2

5

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);
trincot
  • 317,000
  • 35
  • 244
  • 286
2

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.

tevemadar
  • 12,389
  • 3
  • 21
  • 49