[1e20, 1, -1e20].reduce((a, b) => a + b, 0); // 0
[1e17, 10].reduce((a, b) => a + b, 0); // 100000000000000020
は?なんだこれバグか?
ということで、配列に対してできるだけ正確な足し算ができるメソッドMath.sumPreciseが提案されています。
現在の進捗状況は、ステージは3であり仕様は確定しています。
Firefoxは2025/04/01リリースのFirefox 137で対応済です。
Safariはドキュメントでは2025/03/31リリースの18.4で対応したって言ってるんだけどCan I useやMDNでは未対応ってなっていてよくわかりません。
なんにしろmergeはされているのでそのうち動くと思います。
Chromeは実装すらもまだです。
やっぱ金にならんことには遅いなChrome。
ということで以下は該当のProposal、Math.sumPreciseの紹介です。
Math.sumPrecise
複数の値を合計するメソッドを追加する提案。
Status
このproposalはTC39プロセスのステージ3です。
テストはこちら、実装状況はこちらです。
Motivation
リストの合計を計算することは非常に一般的であり、Array.prototype.reduce
に僅かに残っている有用なユースケースのひとつです。
しかし、操作をユーザが直接記述できるようにしたほうがよいでしょう。
浮動小数の合計の計算は、ちゃんとしたアルゴリズムを使用すれば.reduce((a, b) => a + b, 0)
よりずっと正確に行えますが、それを知っているJavaScriptユーザは多くありません。
また、知っている人でもわざわざ実装しようとしたりしません。
より良い方法を、みんなが簡単に利用できた方がよいでしょう。
Proposal
正確なアルゴリズムを使用して、反復可能なオブジェクトの合計値を返すメソッドMath.sumPrecise
を追加します。
let values = [1e20, 0.1, -1e20];
values.reduce((a, b) => a + b, 0); // 0
Math.sumPrecise(values); // 0.1
Questions
Which algorithm?
どのアルゴリズムを使用する?
proposalでは特定のアルゴリズムを指定しません。
最大限に正しい答え、つまり任意精度演算を行い、その結果を浮動小数に戻した値と等しくなるアルゴリズムが許可されます。
実際は同じ精度の演算を、任意精度演算を使用せずに計算できることが知られています。
方法のひとつがShewchuk '96であり、PolyfillではこれをJavaScriptで実装しています。
Pythonではmath.fsumが同じアルゴリズムで実装されていますが、こちらは中間オーバーフローを正しく処理していません。
より高速なアルゴリズムがFast exact summation using small and large superaccumulatorsで公開されており、MITライセンスの実装が入手可能です。
Iterable-taking or variadic?
引数は配列か、可変長か。
Math.max
では引数として可変長引数が推奨されていますが、これは好ましくありません。
要素数が数万程度でスタックオーバーフローやRangeErrorが発生する可能性があります。
そのため、本proposalではIterableのみを受け取ります。
Naming
命名はMath.sum
がもっとも単純でわかりやすいですが、それでは正確で遅いアルゴリズムを使っていることが伝わりません。
そのためMath.sumPrecise
という名前を付けました。
Should this coerce things to number, or throw if given something which is not a number?
数値以外の引数は数値に変換する?エラーを吐く?
Math.max
とは異なり、数値以外の値は受け付けません。
Is the sum of an empty list 0 or -0?
空は0ですか、-0ですか。
-0です。
これによって、Math.sumPrecise([]) + Math.sumPrecise(foo)
とMath.sumPrecise(foo)
が常に同じ値であることが保証されます。
Should this work with BigInts?
BigIntで動作しますか?
いいえ。
Math.sumPrecise([])
が-0を返すため、5n + Math.sumPrecise(bigints)
はbigintsが空のときだけエラーになってしまいます。
BigIntの合計を計算する別のメソッドを用意することはできるでしょうが、本proposalには含まれません。
感想
JavaScriptにはBigIntという型があり、これは名前に反して任意精度整数なので、正確な計算を行いたいならこれを使うことができます。
とはいえ、そこまで精度を求めない普通の計算でいちいちBigIntを使うのも面倒ですし、とりあえずよくある使い道に手っ取り早い解決策を用意するのは悪いことではないでしょう。
合計を出すことはよくありますが、最初に大きい値を持ってくるだけで露骨におかしくなるので、気軽に正確な値を出せる関数を追加するのは便利でいい対応だと思います。
ちなみに冒頭の例のうち、後者はMath.sumPrecise
を使っても正しくなりません。
Math.sumPrecise([1e17, 10]) // 100000000000000020
まあ、そもそも計算しなくても間違う仕様なのですけどね。
100000000000000010; // 100000000000000020