【ITニュース解説】Your Sum Depends on the Order. Math.sumPrecise() Doesn't.
2026年10月12日に「Dev.to」が公開したITニュース「Your Sum Depends on the Order. Math.sumPrecise() Doesn't.」について初心者にもわかりやすく解説しています。
ITニュース概要
JavaScriptで複数の数値を合計する際、足す順序によってわずかな計算誤差が生じることがある。新機能`Math.sumPrecise()`は、この浮動小数点数計算の誤差を防ぎ、どんな順序でも常に正確な合計値を返す。多くの数値を扱うシステム開発で、信頼性の高い合計計算を実現するのに役立つ。ブラウザで利用可能だ。
ITニュース解説
JavaScriptでプログラミングを行う際、数値を合計する処理は頻繁に登場する基本的な操作だ。しかし、この一見単純な計算には、実はシステム開発において重大な問題を引き起こす可能性のある落とし穴が存在する。それは、計算の順序によって合計結果がわずかに変わってしまうという現象で、コンピュータが数値を扱う「浮動小数点数」という仕組みに深く関係している。この長年の課題に対し、新しいJavaScriptの機能であるMath.sumPrecise()が導入され、正確な合計計算を可能にする解決策を提供している。
まず、この問題が具体的にどのような形で現れるかを見てみよう。例えば、オンラインショッピングカートの合計金額を計算する場面を想像してほしい。商品リストの価格をJavaScriptのreduce()メソッドを使って合計すると、特定の金額、例えば48.54という結果が得られたとする。ところが、ユーザーが何らかの理由で商品リストを「価格順」に並べ替えてから同じように合計を計算し直すと、結果が48.53999999999999のように、ごくわずかにずれてしまうことがある。同じ商品、同じ個数であるにもかかわらず、並べ替えという操作だけで合計金額が変わってしまうのは、システムの信頼性に関わる重大な問題である。
この現象の背景には、コンピュータが小数を表現する「浮動小数点数」の仕組みがある。私たちは普段10進数を使うが、コンピュータは内部で2進数で数を処理する。この2進数では、0.1や0.2のようなシンプルな10進数の小数を完全に正確に表現できないことが多く、ごくわずかな誤差を含む近似値として扱われる。例えるなら、正確な長さを測りたいのに、目盛りが不完全な定規を使っているような状態だ。そして、reduce()メソッドのように複数の数値を順番に足していく場合、一つ一つの加算が行われるたびに、このごくわずかな誤差が「丸め」られてしまう。この小さな丸め誤差が積み重なり、さらに加算の順序が変わると丸めのタイミングやその結果も変わるため、最終的な合計値も異なってしまうのだ。この問題は、特に非常に大きな数と非常に小さな数が混ざって加算される場合に顕著になる。例えば、非常に大きな数である1e20(1の後に0が20個続く数)に0.1を足し、その後-1e20を足すようなケースでは、1e20 + 0.1の時点で0.1が無視されて1e20に戻ってしまい、最終的な結果が0になってしまうことがある。本来なら0.1が残るはずなのに、小さな数値が計算から「消えてしまう」のだ。
このような浮動小数点数の合計計算における精度と安定性の問題を解決するために開発されたのが、新しいJavaScriptの機能Math.sumPrecise()である。この機能は、数値のリスト(イテラブル)を一つ受け取り、その正確な合計を計算して返す。Math.sumPrecise()の最大の特徴は、計算の途中で数値を丸める処理を一切行わず、すべての加算を高い精度で行った後、最後に一度だけ最終結果を浮動小数点数として丸める点にある。この仕組みにより、どのような順序で数値が与えられても、常に同じ、そして数学的に正確な合計値が得られるようになるのだ。
先ほどのショッピングカートの例でMath.sumPrecise()を使えば、商品を並べ替えようが、元の順序のままだろうが、常に正確な48.54という合計金額が得られる。また、[1e20, 0.1, -1e20]の例も、Math.sumPrecise()を使えば正しく0.1という結果が返される。従来のreduce()で0.1を10回足すと0.9999999999999999といった誤差が生じることがあったが、Math.sumPrecise()であれば正確に1を返す。1000回足した場合も、reduce()が99.9999999999986となるのに対し、Math.sumPrecise()は正確に100を返す。このように、Math.sumPrecise()は、開発者が合計計算に対して当然だと考える「正確さ」を、現実のコードで実現してくれる機能なのだ。
Math.sumPrecise()は、配列だけでなく、Mapオブジェクトの値やジェネレーター関数から生成される値など、JavaScriptのあらゆる「イテラブル」な数値のリストを受け取ることができるため、既存の様々なコードに柔軟に組み込むことが可能である。さらに、計算の途中で一時的に非常に大きな数になり、通常の計算では「無限大(Infinity)」として扱われてしまうようなケースでも、Math.sumPrecise()は最終的な結果が有限の数値に収まる限り、それを正確に計算できる耐性を持っている。性能面でも、あるテストでは従来のreduce()を使った方法よりも高速に動作するという結果も出ており、ただ正確なだけでなく、効率も良い場合がある。
ただし、Math.sumPrecise()を利用する際にはいくつか知っておくべき点がある。まず、この機能は比較的新しいため、すべての実行環境で利用できるわけではない。主要なブラウザの比較的新しいバージョンではサポートされているが、Node.jsではまだ標準でサポートされていない場合がある。そのため、コードを実行する環境によっては、Math.sumPrecise()が利用可能かを確認し、利用できない場合は従来のreduce()などを使ったフォールバック処理を実装する必要がある。しかし、フォールバックでは誤差の問題が再発する可能性があるため、その場合はユーザーに厳密な正確さを約束することはできない。
また、Math.sumPrecise()は、0.1 + 0.2が0.30000000000000004となるような、浮動小数点数表現そのものに起因する誤差を解決するものではない。これは、0.1という数値自体がコンピュータの内部で完全に正確に表現できないことに由来するものであり、Math.sumPrecise()は「加算処理の際に積み重なる丸め誤差」を取り除くことに特化している。つまり、個々の数値がすでに近似値である場合、その近似値同士の正確な合計を出す、という働きをする。
他にも、Math.sumPrecise()は数値のリストしか受け付けず、文字列などの数ではない値を渡すとエラーになる。フォームの入力値など、文字列として扱われる可能性のあるデータは、事前に数値に変換しておく必要がある。また、複数の引数をバラバラに渡すのではなく、Math.max()のように一つのイテラブルとして渡す必要がある点も注意が必要だ。空のリストを渡した場合は-0という結果になるが、これはほとんどの場合0と等価に扱われるため問題になることは少ない。InfinityやNaNといった特殊な値が含まれる場合の挙動は、通常の数学的なルールに従う。
最後に重要な点として、もし金融取引など「お金」に関する非常に厳密な計算を行う必要がある場合は、Math.sumPrecise()を使っても完全に安心できるわけではない。このような場面では、小数点以下を扱わず、すべてを「セント」や「円」のような最小の整数単位で表現・計算する「整数ベースの通貨計算」が、依然として最も安全で推奨される方法である。Math.sumPrecise()は浮動小数点数の合計を「正しく、安定して」計算するための強力なツールだが、根本的な誤差をゼロにする方法とは異なる点に留意すべきだ。
結論として、Math.sumPrecise()は、これまでのJavaScriptにおける合計計算で開発者が「こうなってほしい」と期待していた、計算順序に左右されない正確な合計値を提供する画期的な機能である。システムの数値計算の信頼性を向上させるために、コードベース内でreduce((a, b) => a + b, 0)のような合計計算を見つけたら、この新しいMath.sumPrecise()への置き換えを積極的に検討することが推奨される。