Fraction.js - JavaScript 中的 ℚ
你是否觉得浮点运算的局限性令人沮丧,尤其是当 π 或 √2 这样的有理数和无理数被存储在同一有限精度中时?这可能导致可以避免的不准确,例如:
1 / 98 * 98 // Results in 0.9999999999999999
对于需要更高精度或更倾向于处理分数的应用程序,请考虑将 Fraction.js 集成到您的项目中。
该库有效地解决了精度问题,如下所示:
Fraction(1).div(98).mul(98) // Returns 1
Fraction.js 对分子和分母均采用 BigInt 表示法,在确保最小性能开销的同时最大化精度。其设计针对精度进行了优化,使其成为其他数学工具(如 Polynomial.js 和 Math.js)的理想基础库。
将小数转换为分数
Fraction.js 的核心功能之一是其能够将小数无缝转换为分数。
let x = new Fraction(1.88);
let res = x.toFraction(true); // Returns "1 22/25" as a string
这在需要精确的分数表示,而非处理浮点算术的局限性时特别有用。如果你允许一定的误差容限呢?
let x = new Fraction(0.33333);
let res = x.simplify(0.001) // Error < 0.001
.toFraction(); // Returns "1/3" as a string
精度
随着 JavaScript 中原生 BigInt 支持的普及,Fraction.js 等库利用它来处理更高精度的计算。这提升了大数数学运算的速度和准确性,为那些需要比浮点数更高精度的任务提供了更好的解决方案。
示例 / 动机
使用 Fraction.js 的一个简单示例可能如下所示:
var f = new Fraction("9.4'31'"); // 9.4313131313131...
f.mul([-4, 3]).mod("4.'8'"); // 4.88888888888888...
然后,结果可以显示为:
console.log(f.toFraction()); // -4154 / 1485
此外,您可以访问分数的内部属性,例如符号(s)、分子(n)和分母(d)。请记住,这些值以 BigInt 形式存储:
Number(f.s) * Number(f.n) / Number(f.d) = -1 * 4154 / 1485 = -2.797306...
如果你尝试使用浮点运算手动计算,你会得到类似以下内容:
(9.4313131 * (-4 / 3)) % 4.888888 = -2.797308133...
虽然结果相对接近,但不及 Fraction.js 提供的基于分数的方法准确,尤其是在处理循环小数或复杂运算时。这凸显了该库所带来的精度价值。
拉普拉斯概率
下面是一个使用 Fraction.js 计算概率的简单示例。让我们确定在公平骰子上掷出特定结果的概率:
- P({3}):掷出 3 的概率。
- P({1, 4}):掷出 1 或 4 的概率。
- P({2, 4, 6}):掷出 2、4 或 6 的概率。
P({3}):
var p = new Fraction([3].length, 6).toString(); // "0.1(6)"
P({1, 4}):
var p = new Fraction([1, 4].length, 6).toString(); // "0.(3)"
P({2, 4, 6}):
var p = new Fraction([2, 4, 6].length, 6).toString(); // "0.5"
将度/分/秒转换为精确的有理数表示:
57+45/60+17/3600
var deg = 57; // 57°
var min = 45; // 45 Minutes
var sec = 17; // 17 Seconds
new Fraction(deg).add(min, 60).add(sec, 3600).toString() // -> 57.7547(2)
无理数的有理逼近
要用分子和分母来近似像 sqrt(5) - 2 这样的数,你可以将方程重写为:pow(n / d + 2, 2) = 5。
然后,以下算法将生成该有理数及其二进制表示。
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)
结果是
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
因此,二分法经过 11 次迭代后的近似值为 483 / 2048,其二进制表示为 0.00111100011(参见 WolframAlpha)
我在我的 blog 上发布了另一个使用 fraction.js 近似 PI 的示例(虽然近似无理数仍不是最佳方案,但它完美地展示了 Fraction.js 的功能)。
获取数字的精确小数部分
var f = new Fraction("-6.(3416)");
console.log(f.mod(1).abs().toFraction()); // = 3416/9999
数学上正确的取模
对于负数同余的处理方式与计算机科学中大多数取模实现不同。即使是 Fraction.js 的 mod() 函数也遵循典型的处理方式。要解决在 Fraction.js 中获得数学上正确取模的问题,你可以采用以下方法:
var a = -1;
var b = 10.99;
console.log(new Fraction(a)
.mod(b)); // Not correct, usual Modulo
console.log(new Fraction(a)
.mod(b).add(b).mod(b)); // Correct! Mathematical Modulo
fmod() 精度问题已规避
事实证明,Fraction.js 的性能几乎优于任何 fmod() 实现,包括 JavaScript 本身、php.js、C++、Python、Java 甚至 Wolframalpha,这是因为诸如 0.05、0.1 等数字在二进制下是无限小数。
方程 fmod(4.55, 0.05) 的结果为 0.04999999999999957,而 wolframalpha 给出的结果是 1/20。正确答案应为 零,因为 0.05 能整除 4.55 且无余数。
解析器
任何函数(见下文)以及 Fraction 类的构造函数都会解析其输入并将其化简为最简分数。
您可以传入数组、对象、整数、双精度浮点数或字符串。
数组 / 对象
new Fraction(numerator, denominator);
new Fraction([numerator, denominator]);
new Fraction({n: numerator, d: denominator});
整数
new Fraction(123);
双打
new Fraction(55.4);
注意: 如果你直接传入 double 类型,Fraction.js 将基于 Farey Sequences 执行数值分析。如果你关注性能,请缓存 Fraction.js 对象并传入数组/对象。
该方法非常精确,但过大的精确数值,例如 1234567.9991829 会导致错误的近似值。如果你想保持数值原样,请将其转换为字符串,因为字符串解析器不会执行任何进一步的观察。如果你在近似方面遇到问题,在文件 examples/approx.js 中有一个不同的近似算法,它可能在某些更具体的用例中表现更好。
字符串
new Fraction("123.45");
new Fraction("123/45"); // A rational number represented as two decimals, separated by a slash
new Fraction("123:45"); // A rational number represented as two decimals, separated by a colon
new Fraction("4 123/45"); // A rational number represented as a whole number and a fraction
new Fraction("123.'456'"); // Note the quotes, see below!
new Fraction("123.(456)"); // Note the brackets, see below!
new Fraction("123.45'6'"); // Note the quotes, see below!
new Fraction("123.45(6)"); // Note the brackets, see below!
两个参数
new Fraction(3, 2); // 3/2 = 1.5
循环小数
Fraction.js 可以轻松处理循环小数。例如 1/3 是 0.3333...。只有一个循环数字。正如上面示例所示,你可以将像 1/3 这样的数字作为 "0.'3'" 或 "0.(3)" 传入,二者是同义词。没有测试来解析像 0.166666666 这样的数字为 1/6!如果你确实想处理这个数字,请自行使用下方的函数添加括号,例如:0.1(66666666)
假设你想计算 123.32 / 33.6(567)。WolframAlpha 指出你将得到一个 1776 位的循环节。Fraction.js 得出相同的结果。试一试:
var f = new Fraction("123.32");
console.log("Bam: " + f.div("33.6(567)"));
为了自动将类似 "0.123123123" 的数字转换为对 Fraction.js 更友好的形式,例如 "0.(123)",我在 10 分钟内编写了这个小型暴力算法。欢迎提出改进建议...
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); // Silly String.repeat hack
if (0 === (offset + repeat).indexOf(str)) {
return pre + offset + "(" + pad + ")";
}
}
}
return null;
}
var f, x = formatDecimal("13.0123123123"); // = 13.0(123)
if (x !== null) {
f = new Fraction(x);
}
属性
Fraction 对象允许直接访问分子、分母和符号属性。确保只有符号属性持有符号信息,因此只需针对该属性进行符号比较。
var f = new Fraction('-1/2');
console.log(f.n); // Numerator: 1
console.log(f.d); // Denominator: 2
console.log(f.s); // Sign: -1
函数
Fraction abs()
返回不带任何符号信息的实际数值
Fraction neg()
返回符号翻转后的实际数值,以获取加法逆元
Fraction add(n)
返回实际数值与参数 n 的和
Fraction sub(n)
返回实际数值与参数 n 的差
Fraction mul(n)
返回实际数值与参数 n 的积
Fraction div(n)
返回实际数值与参数 n 的商
Fraction pow(exp)
返回实际数值的幂,指数为可能的有理数。如果结果变为非有理数,则函数返回 null。
Fraction log(base)
返回实际数值以给定有理数为底的对数。如果结果变为非有理数,则函数返回 null。
Fraction mod(n)
返回实际对象与 n 的模(除法的余数)(this % n)。如果您喜欢,它比 fmod() 精确得多。请注意,mod() 与大多数编程语言的取模运算符相同。如果您想要数学上正确的取模,请参阅 此处。
Fraction mod()
返回实际对象的模(分子模分母)
Fraction gcd(n)
返回分数的最大公约数
Fraction lcm(n)
返回分数的最小公倍数
Fraction ceil([places=0-16])
使用 Math.ceil 返回有理数的上取整值
Fraction floor([places=0-16])
返回有理数的向下取整值,使用 Math.floor
Fraction round([places=0-16])
返回使用 Math.round 四舍五入后的有理数
Fraction roundTo(multiple)
将有理数四舍五入到另一个有理数的最近倍数。
Fraction inverse()
返回当前数的乘法逆元(n / d 变为 d / n),以获取倒数
Fraction simplify([eps=0.001])
在特定误差阈值下简化有理数。例如,0.333 在 eps=0.001 下将变为 1/3
boolean equals(n)
检查两个有理数是否相等
boolean lt(n)
检查此有理数是否小于另一个有理数
boolean lte(n)
检查此有理数是否小于或等于另一个有理数
boolean gt(n)
检查此有理数是否大于另一个有理数
boolean gte(n)
检查此有理数是否大于或等于另一个有理数
int compare(n)
比较两个数。
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)
检查两个数是否可整除(n 整除此数)
double valueOf()
返回分数的十进制表示
String toString([decimalPlaces=15])
生成给定对象的精确字符串表示。对于循环小数,循环周期内的数字将用括号括起来,例如 1/3 = "0.(3)"。对于其他数字,字符串将包含最多指定的 decimalPlaces 位有效数字,如果发生截断,则包括末尾的零。例如,1/2 将被表示为 "0.5",没有额外的末尾零。
注意: 由于同时提供了 valueOf() 和 toString(),toString() 仅在对象用于字符串上下文时隐式调用。例如,当使用加号运算符如 "123" + new Fraction 时,会先调用 valueOf(),因为 JavaScript 在拼接之前会尝试合并原始值,且字符串类型具有优先权。然而,alert(new Fraction) 或 String(new Fraction) 将按预期工作。为确保特定行为,请显式调用 toString() 或 valueOf()。
String toLatex(showMixed=false)
生成实际对象的精确 LaTeX 表示。你可以在我的博客上查看 实时演示。
可选的布尔参数指示是否显示带分数。显示 "1 1/3" 而不是 "4/3"
String toFraction(showMixed=false)
获取分数的字符串表示
可选的布尔参数指示是否显示带分数。显示 "1 1/3" 而不是 "4/3"
Array toContinued()
获取表示为连分数的分数的数组。第一个元素始终包含整数部分。
var f = new Fraction('88/33');
var c = f.toContinued(); // [2, 1, 2]
Fraction clone()
创建实际 Fraction 对象的副本
异常
如果发生严重错误(解析错误、除以零),Fraction.js 会抛出异常!请确保你正确处理它们。
安装
你可以通过 npm 安装 Fraction.js:
npm install fraction.js
或者使用 yarn:
yarn add fraction.js
或者,下载或克隆该仓库:
git clone https://github.com/rawify/Fraction.js
用法
在你的项目中包含 fraction.min.js 文件:
<script src="path/to/fraction.min.js"></script>
<script>
var x = new Fraction("13/4");
</script>
或者在 Node.js 项目中:
const Fraction = require('fraction.js');
或
import Fraction from 'fraction.js';
编码风格
与我所有的库一样,Fraction.js 的编写旨在通过 Google Closure Compiler 的高级模式压缩后最小化体积。代码风格经过优化以最大化可压缩性。如果你扩展该库,请保持此风格。
构建库
克隆 Git 仓库后,运行:
npm install
npm run build
运行测试
针对随附的测试套件测试源代码,操作如下
npm run test
版权与许可
版权所有 (c) 2025, Robert Eisele 依据 MIT 许可证授权。