Fraction.js - ℚ in JavaScript
Do you find the limitations of floating-point arithmetic frustrating, especially when rational and irrational numbers like π or √2 are stored within the same finite precision? This can lead to avoidable inaccuracies such as:
1 / 98 * 98
For applications requiring higher precision or where working with fractions is preferable, consider incorporating Fraction.js into your project. Integration is straightforward:
import Fraction from 'fraction.js';
var Fraction = require('fraction.js');
The library effectively addresses precision issues, as demonstrated below:
Fraction(1).div(98).mul(98)
Fraction.js uses a BigInt
representation for both the numerator and denominator, ensuring minimal performance overhead while maximizing accuracy. Its design is optimized for precision, making it an ideal choice as a foundational library for other math tools, such as Polynomial.js and Math.js.
Convert Decimal to Fraction
One of the core features of Fraction.js is its ability to seamlessly convert decimal numbers into fractions.
let x = new Fraction(1.88);
let res = x.toFraction(true);
This is particularly useful when you need precise fraction representations instead of dealing with the limitations of floating-point arithmetic. What if you allow some error tolerance?
let x = new Fraction(0.33333);
let res = x.simplify(0.001)
.toFraction();
Precision
As native BigInt
support in JavaScript becomes more common, libraries like Fraction.js use it to handle calculations with higher precision. This improves the speed and accuracy of math operations with large numbers, providing a better solution for tasks that need more precision than floating-point numbers can offer.
Examples / Motivation
A simple example of using Fraction.js might look like this:
var f = new Fraction("9.4'31'");
f.mul([-4, 3]).mod("4.'8'");
The result can then be displayed as:
console.log(f.toFraction());
Additionally, you can access the internal attributes of the fraction, such as the sign (s), numerator (n), and denominator (d). Keep in mind that these values are stored as BigInt
:
Number(f.s) * Number(f.n) / Number(f.d) = -1 * 4154 / 1485 = -2.797306...
If you attempted to calculate this manually using floating-point arithmetic, you'd get something like:
(9.4313131 * (-4 / 3)) % 4.888888 = -2.797308133...
While the result is reasonably close, it’s not as accurate as the fraction-based approach that Fraction.js provides, especially when dealing with repeating decimals or complex operations. This highlights the value of precision that the library brings.
Laplace Probability
Here's a straightforward example of using Fraction.js to calculate probabilities. Let's determine the probability of rolling a specific outcome on a fair die:
- P({3}): The probability of rolling a 3.
- P({1, 4}): The probability of rolling either 1 or 4.
- P({2, 4, 6}): The probability of rolling 2, 4, or 6.
P({3}):
var p = new Fraction([3].length, 6).toString();
P({1, 4}):
var p = new Fraction([1, 4].length, 6).toString();
P({2, 4, 6}):
var p = new Fraction([2, 4, 6].length, 6).toString();
Convert degrees/minutes/seconds to precise rational representation:
57+45/60+17/3600
var deg = 57;
var min = 45;
var sec = 17;
new Fraction(deg).add(min, 60).add(sec, 3600).toString()
Rational approximation of irrational numbers
Now it's getting messy ;d To approximate a number like sqrt(5) - 2 with a numerator and denominator, you can reformat the equation as follows: pow(n / d + 2, 2) = 5.
Then the following algorithm will generate the rational number besides the binary representation.
var x = "/", s = "";
var a = new Fraction(0),
b = new Fraction(1);
for (var n = 0; n <= 10; n++) {
var c = a.add(b).div(2);
console.log(n + "\t" + a + "\t" + b + "\t" + c + "\t" + x);
if (c.add(2).pow(2).valueOf() < 5) {
a = c;
x = "1";
} else {
b = c;
x = "0";
}
s+= x;
}
console.log(s)
The result is
n a[n] b[n] c[n] x[n]
0 0/1 1/1 1/2 /
1 0/1 1/2 1/4 0
2 0/1 1/4 1/8 0
3 1/8 1/4 3/16 1
4 3/16 1/4 7/32 1
5 7/32 1/4 15/64 1
6 15/64 1/4 31/128 1
7 15/64 31/128 61/256 0
8 15/64 61/256 121/512 0
9 15/64 121/512 241/1024 0
10 241/1024 121/512 483/2048 1
Thus the approximation after 11 iterations of the bisection method is 483 / 2048 and the binary representation is 0.00111100011 (see WolframAlpha)
I published another example on how to approximate PI with fraction.js on my blog (Still not the best idea to approximate irrational numbers, but it illustrates the capabilities of Fraction.js perfectly).
Get the exact fractional part of a number
var f = new Fraction("-6.(3416)");
console.log(f.mod(1).abs().toFraction());
Mathematical correct modulo
The behaviour on negative congruences is different to most modulo implementations in computer science. Even the mod() function of Fraction.js behaves in the typical way. To solve the problem of having the mathematical correct modulo with Fraction.js you could come up with this:
var a = -1;
var b = 10.99;
console.log(new Fraction(a)
.mod(b));
console.log(new Fraction(a)
.mod(b).add(b).mod(b));
fmod() impreciseness circumvented
It turns out that Fraction.js outperforms almost any fmod() implementation, including JavaScript itself, php.js, C++, Python, Java and even Wolframalpha due to the fact that numbers like 0.05, 0.1, ... are infinite decimal in base 2.
The equation fmod(4.55, 0.05) gives 0.04999999999999957, wolframalpha says 1/20. The correct answer should be zero, as 0.05 divides 4.55 without any remainder.
Parser
Any function (see below) as well as the constructor of the Fraction class parses its input and reduce it to the smallest term.
You can pass either Arrays, Objects, Integers, Doubles or Strings.
Arrays / Objects
new Fraction(numerator, denominator);
new Fraction([numerator, denominator]);
new Fraction({n: numerator, d: denominator});
Integers
new Fraction(123);
Doubles
new Fraction(55.4);
Note: If you pass a double as it is, Fraction.js will perform a number analysis based on Farey Sequences. If you concern performance, cache Fraction.js objects and pass arrays/objects.
The method is really precise, but too large exact numbers, like 1234567.9991829 will result in a wrong approximation. If you want to keep the number as it is, convert it to a string, as the string parser will not perform any further observations. If you have problems with the approximation, in the file examples/approx.js
is a different approximation algorithm, which might work better in some more specific use-cases.
Strings
new Fraction("123.45");
new Fraction("123/45");
new Fraction("123:45");
new Fraction("4 123/45");
new Fraction("123.'456'");
new Fraction("123.(456)");
new Fraction("123.45'6'");
new Fraction("123.45(6)");
Two arguments
new Fraction(3, 2);
Repeating decimal places
Fraction.js can easily handle repeating decimal places. For example 1/3 is 0.3333.... There is only one repeating digit. As you can see in the examples above, you can pass a number like 1/3 as "0.'3'" or "0.(3)", which are synonym. There are no tests to parse something like 0.166666666 to 1/6! If you really want to handle this number, wrap around brackets on your own with the function below for example: 0.1(66666666)
Assume you want to divide 123.32 / 33.6(567). WolframAlpha states that you'll get a period of 1776 digits. Fraction.js comes to the same result. Give it a try:
var f = new Fraction("123.32");
console.log("Bam: " + f.div("33.6(567)"));
To automatically make a number like "0.123123123" to something more Fraction.js friendly like "0.(123)", I hacked this little brute force algorithm in a 10 minutes. Improvements are welcome...
function formatDecimal(str) {
var comma, pre, offset, pad, times, repeat;
if (-1 === (comma = str.indexOf(".")))
return str;
pre = str.substr(0, comma + 1);
str = str.substr(comma + 1);
for (var i = 0; i < str.length; i++) {
offset = str.substr(0, i);
for (var j = 0; j < 5; j++) {
pad = str.substr(i, j + 1);
times = Math.ceil((str.length - offset.length) / pad.length);
repeat = new Array(times + 1).join(pad);
if (0 === (offset + repeat).indexOf(str)) {
return pre + offset + "(" + pad + ")";
}
}
}
return null;
}
var f, x = formatDecimal("13.0123123123");
if (x !== null) {
f = new Fraction(x);
}
Attributes
The Fraction object allows direct access to the numerator, denominator and sign attributes. It is ensured that only the sign-attribute holds sign information so that a sign comparison is only necessary against this attribute.
var f = new Fraction('-1/2');
console.log(f.n);
console.log(f.d);
console.log(f.s);
Functions
Fraction abs()
Returns the actual number without any sign information
Fraction neg()
Returns the actual number with flipped sign in order to get the additive inverse
Fraction add(n)
Returns the sum of the actual number and the parameter n
Fraction sub(n)
Returns the difference of the actual number and the parameter n
Fraction mul(n)
Returns the product of the actual number and the parameter n
Fraction div(n)
Returns the quotient of the actual number and the parameter n
Fraction pow(exp)
Returns the power of the actual number, raised to an possible rational exponent. If the result becomes non-rational the function returns null
.
Fraction log(base)
Returns the logarithm of the actual number to a given rational base. If the result becomes non-rational the function returns null
.
Fraction mod(n)
Returns the modulus (rest of the division) of the actual object and n (this % n). It's a much more precise fmod() if you like. Please note that mod() is just like the modulo operator of most programming languages. If you want a mathematical correct modulo, see here.
Fraction mod()
Returns the modulus (rest of the division) of the actual object (numerator mod denominator)
Fraction gcd(n)
Returns the fractional greatest common divisor
Fraction lcm(n)
Returns the fractional least common multiple
Fraction ceil([places=0-16])
Returns the ceiling of a rational number with Math.ceil
Fraction floor([places=0-16])
Returns the floor of a rational number with Math.floor
Fraction round([places=0-16])
Returns the rational number rounded with Math.round
Fraction roundTo(multiple)
Rounds a fraction to the closest multiple of another fraction.
Fraction inverse()
Returns the multiplicative inverse of the actual number (n / d becomes d / n) in order to get the reciprocal
Fraction simplify([eps=0.001])
Simplifies the rational number under a certain error threshold. Ex. 0.333
will be 1/3
with eps=0.001
boolean equals(n)
Check if two rational numbers are equal
boolean lt(n)
Check if this rational number is less than another
boolean lte(n)
Check if this rational number is less than or equal another
boolean gt(n)
Check if this rational number is greater than another
boolean gte(n)
Check if this rational number is greater than or equal another
int compare(n)
Compare two numbers.
result < 0: n is greater than actual number
result > 0: n is smaller than actual number
result = 0: n is equal to the actual number
boolean divisible(n)
Check if two numbers are divisible (n divides this)
double valueOf()
Returns a decimal representation of the fraction
String toString([decimalPlaces=15])
Generates an exact string representation of the given object. For repeating decimal places, digits within repeating cycles are enclosed in parentheses, e.g., 1/3 = "0.(3)"
. For other numbers, the string will include up to the specified decimalPlaces
significant digits, including any trailing zeros if truncation occurs. For example, 1/2
will be represented as "0.5"
, without additional trailing zeros.
Note: Since both valueOf()
and toString()
are provided, toString()
will only be invoked implicitly when the object is used in a string context. For instance, when using the plus operator like "123" + new Fraction
, valueOf()
will be called first, as JavaScript attempts to combine primitives before concatenating them, with the string type taking precedence. However, alert(new Fraction)
or String(new Fraction)
will behave as expected. To ensure specific behavior, explicitly call either toString()
or valueOf()
.
String toLatex(showMixed=false)
Generates an exact LaTeX representation of the actual object. You can see a live demo on my blog.
The optional boolean parameter indicates if you want to show the a mixed fraction. "1 1/3" instead of "4/3"
String toFraction(showMixed=false)
Gets a string representation of the fraction
The optional boolean parameter indicates if you want to showa mixed fraction. "1 1/3" instead of "4/3"
Array toContinued()
Gets an array of the fraction represented as a continued fraction. The first element always contains the whole part.
var f = new Fraction('88/33');
var c = f.toContinued();
Fraction clone()
Creates a copy of the actual Fraction object
Exceptions
If a really hard error occurs (parsing error, division by zero), Fraction.js throws exceptions! Please make sure you handle them correctly.
Installation
Installing fraction.js is as easy as cloning this repo or use the following command:
npm install fraction.js
Using Fraction.js with the browser
<script src="fraction.min.js"></script>
<script>
console.log(Fraction("123/456"));
</script>
Using Fraction.js with TypeScript
import Fraction from "fraction.js";
console.log(Fraction("123/456"));
Coding Style
As every library I publish, Fraction.js is also built to be as small as possible after compressing it with Google Closure Compiler in advanced mode. Thus the coding style orientates a little on maxing-out the compression rate. Please make sure you keep this style if you plan to extend the library.
Building the library
After cloning the Git repository run:
npm install
npm run build
Run a test
Testing the source against the shipped test suite is as easy as
npm run test
Copyright and Licensing
Copyright (c) 2024, Robert Eisele
Licensed under the MIT license.