大抵の金額計算は JavaScript の精度でも十分だと思う
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ decimal でも有効桁数以下は丸められるだけ
◆ C# や Python を見ると 28 桁ほどあるけどそんなに必要なことはほぼない
◆ 0.1 が正確に表現できず誤差はあるけどほんの僅か
◆ 影響出るほどの計算を繰り返すことは現実的に考えづらい
◆ 心配なら計算の度に toPrecision や toFixed で文字列化してから再度数値化すればいい
◆ 注意するのは小数に対して一致確認や比較するとき
◆ 文字列比較や再数値化しないと誤差の影響で求めてる結果にならないかも
◆ 簡単に対処できるものなのでわざわざライブラリを入れたりサーバ側で処理するほどじゃない
◆ C# や Python を見ると 28 桁ほどあるけどそんなに必要なことはほぼない
◆ 0.1 が正確に表現できず誤差はあるけどほんの僅か
◆ 影響出るほどの計算を繰り返すことは現実的に考えづらい
◆ 心配なら計算の度に toPrecision や toFixed で文字列化してから再度数値化すればいい
◆ 注意するのは小数に対して一致確認や比較するとき
◆ 文字列比較や再数値化しないと誤差の影響で求めてる結果にならないかも
◆ 簡単に対処できるものなのでわざわざライブラリを入れたりサーバ側で処理するほどじゃない
JavaScript って数値型は int/float 型の違いがなく number のみで double 型相当なので小数点以下で誤差が出るから金額みたいな正確性が必要な計算に向いてないという話を見かけました
decimal 型はないので そうではあるのですけど 大抵の計算には十分な有効桁数があるので ほとんどの場合は気にするほどでもないと思います
こんな桁の計算をすることはめったにないでしょう
それに decimal は固定小数点で小数点以下◯桁は必ず計算されるか というとそういうものではないです
C# で 1÷7 を計算すると循環小数なのでずっと続いて有効桁数の 28 桁までが表示されます
それに整数の 1234567890 を足すと整数部に 10 桁使われるので小数部の末尾の方は失われます
末尾は丸められます
有効桁数を小さくできてわかりやすい Python で試してみます
デフォルトの丸め挙動は ROUND_HALF_EVEN なので偶数丸めです
四捨五入だと 1 つ目が 1.07 になるはずですが 1.06 です
JavaScript で同じ計算をしてみます
当たり前ですが 有効桁数 3 桁の結果より正確に計算されてますよね
0.001 の桁を四捨五入して 0.01 の桁までにしたいのなら 1.065 を四捨五入して 1.07 にできます
JavaScript ではとても小さいところで誤差が出ていると言っても有効桁数的に問題なければ正確な結果を得られます
1÷7 の計算結果は有効桁数の小さい decimal よりも JavaScript のほうがより下の桁まで計算結果を保持しているので正確です
デフォルトの decimal 型は有効桁数がとても大きいですが その大きさがいらないなら JavaScript で困りません
JavaScript の number 型は 15 ~ 16 桁の有効桁数です
1 億なら 9 桁でまだ小数部に 6 桁ほど残っているので 0.1 や 0.01 の桁までしか扱わないなら十分正確な値です
0.5 は JavaScript でも正確に表現できますが 0.1 はできません
有効桁数外には誤差が存在します
僅かな誤差ですが繰り返し計算すると誤差が大きくなってきます
更に繰り返し回数を増やして 1000 億回 0.1 を足すと
上位 7 桁目に誤差が入ってきました
ですがこれだけの回数 小数値を繰り返し計算することは通常ないと思います
掛け算で 0.1 × 1000 億 とするとこうはならないですし
現実的な回数の計算なら影響出るほどの誤差にはならないはずです
どうしても心配なら計算するたびに toPrecision で精度指定の文字列化をしてから再度数値化することで誤差の蓄積をなくせます
10 桁もあれば十分なのでとりあえず 10 桁にしてます
1÷3 をしてその結果に 3 を掛けます
Python も C# も同様です
有効桁数まで 3 が続いていて それが正確な値として扱われるので 3 を掛けると 9 が並びます
JavaScript だと
と 1 になります
これは JavaScript 以外の言語でも float や double 型を使えば同じ結果になるはずです
ただ たまーにやってしまいがちなのは 小数計算後にそのまま === でリテラルと比較です
有名なものですが 0.1 と 0.2 を足したものが 0.3 と一致しません
前述の通り誤差が存在して その誤差の部分が微妙に違うため異なる値になります
toFixed で小数点以下◯桁までの文字列にして文字列比較をしたり toFixed や toPrecision で文字列化したものを数値化してから比較したりが必要です
この辺が面倒といえば面倒ですが その程度でライブラリを入れたりサーバ側で計算させたりするほうが何倍も面倒だと思います
一応 Proposal には Stage1 ですが Decimal の提案があります
全然進んでないのでいつになるのか不明ですが BigInt みたいに組み込み型として使えるようになればもう少し楽になるかもしれませんね
decimal 型はないので そうではあるのですけど 大抵の計算には十分な有効桁数があるので ほとんどの場合は気にするほどでもないと思います
有効桁数
組み込みで decimal 型のある C# と Python を見てみると C# は 28 ~ 29 桁の有効桁数があり Python は変更可能ですがデフォルトでは 28 桁の有効桁数ですこんな桁の計算をすることはめったにないでしょう
それに decimal は固定小数点で小数点以下◯桁は必ず計算されるか というとそういうものではないです
C# で 1÷7 を計算すると循環小数なのでずっと続いて有効桁数の 28 桁までが表示されます
それに整数の 1234567890 を足すと整数部に 10 桁使われるので小数部の末尾の方は失われます
var num = 1m / 7m;
Console.WriteLine("{0}", num);
// 0.1428571428571428571428571429
num = num + 1234567890m;
Console.WriteLine("{0}", num);
// 1234567890.1428571428571428571
末尾は丸められます
有効桁数を小さくできてわかりやすい Python で試してみます
import decimal
ctx = decimal.getcontext()
ctx.prec = 3
print(ctx.create_decimal(2.13) / 2)
# 1.06
print(ctx.create_decimal(2.15) / 2)
# 1.08
ctx.prec = 10
print(ctx.create_decimal(2.13) / 2)
# 1.065000000
print(ctx.create_decimal(2.15) / 2)
# 1.075000000
デフォルトの丸め挙動は ROUND_HALF_EVEN なので偶数丸めです
四捨五入だと 1 つ目が 1.07 になるはずですが 1.06 です
JavaScript で同じ計算をしてみます
> 2.13/2
1.065
当たり前ですが 有効桁数 3 桁の結果より正確に計算されてますよね
0.001 の桁を四捨五入して 0.01 の桁までにしたいのなら 1.065 を四捨五入して 1.07 にできます
JavaScript ではとても小さいところで誤差が出ていると言っても有効桁数的に問題なければ正確な結果を得られます
1÷7 の計算結果は有効桁数の小さい decimal よりも JavaScript のほうがより下の桁まで計算結果を保持しているので正確です
>>> import decimal
>>> ctx = decimal.getcontext()
>>> ctx.prec = 8
>>> ctx.create_decimal("1") / 7
Decimal('0.14285714')
> 1/7
0.14285714285714285
デフォルトの decimal 型は有効桁数がとても大きいですが その大きさがいらないなら JavaScript で困りません
JavaScript の number 型は 15 ~ 16 桁の有効桁数です
1 億なら 9 桁でまだ小数部に 6 桁ほど残っているので 0.1 や 0.01 の桁までしか扱わないなら十分正確な値です
0.1
他の decimal 型の利点に正確に値を表現できるというものがあります0.5 は JavaScript でも正確に表現できますが 0.1 はできません
> 0.5.toFixed(30)
'0.500000000000000000000000000000'
> 0.1.toFixed(30)
'0.100000000000000005551115123126'
有効桁数外には誤差が存在します
僅かな誤差ですが繰り返し計算すると誤差が大きくなってきます
> let n = 0
undefined
> for (let i=0;i<1000000;i++) n += 0.1
100000.00000133288
更に繰り返し回数を増やして 1000 億回 0.1 を足すと
> let n = 0
undefined
> for (let i=0;i<100_000_000_000;i++) n += 0.1
10000018871.664534
上位 7 桁目に誤差が入ってきました
ですがこれだけの回数 小数値を繰り返し計算することは通常ないと思います
掛け算で 0.1 × 1000 億 とするとこうはならないですし
現実的な回数の計算なら影響出るほどの誤差にはならないはずです
どうしても心配なら計算するたびに toPrecision で精度指定の文字列化をしてから再度数値化することで誤差の蓄積をなくせます
10 桁もあれば十分なのでとりあえず 10 桁にしてます
> let n = 0
undefined
> for (let i=0;i<1000000;i++) {
... n += 0.1
... n = +(n.toPrecision(10))
... }
100000
0.3333...
逆に decimal のほうが不便なところで割り切れない場合の値がもとに戻らないというのがあります1÷3 をしてその結果に 3 を掛けます
>>> import decimal
>>> ctx = decimal.getcontext()
>>> d = ctx.create_decimal("1") / 3
>>> d
Decimal('0.3333333333333333333333333333')
>>> d * 3
Decimal('0.9999999999999999999999999999')
Python も C# も同様です
有効桁数まで 3 が続いていて それが正確な値として扱われるので 3 を掛けると 9 が並びます
JavaScript だと
> const d = 1/3
undefined
> d
0.3333333333333333
> d * 3
1
と 1 になります
これは JavaScript 以外の言語でも float や double 型を使えば同じ結果になるはずです
気をつけるところ
そういう感じなので別に JavaScript でも困ってませんただ たまーにやってしまいがちなのは 小数計算後にそのまま === でリテラルと比較です
有名なものですが 0.1 と 0.2 を足したものが 0.3 と一致しません
> 0.1 + 0.2 === 0.3
false
前述の通り誤差が存在して その誤差の部分が微妙に違うため異なる値になります
> (0.1 + 0.2).toFixed(20)
'0.30000000000000004441'
> 0.3.toFixed(20)
'0.29999999999999998890'
toFixed で小数点以下◯桁までの文字列にして文字列比較をしたり toFixed や toPrecision で文字列化したものを数値化してから比較したりが必要です
> (0.1 + 0.2).toFixed(1) === "0.3"
true
> +(0.1 + 0.2).toPrecision(10) === 0.3
true
この辺が面倒といえば面倒ですが その程度でライブラリを入れたりサーバ側で計算させたりするほうが何倍も面倒だと思います
一応 Proposal には Stage1 ですが Decimal の提案があります
全然進んでないのでいつになるのか不明ですが BigInt みたいに組み込み型として使えるようになればもう少し楽になるかもしれませんね