3049 words
15 minutes
その金額計算、本当に合ってる? JavaScriptの浮動小数点とAIコピペの怖い話

見積システムを作っていて、値引きまわりの計算を実装していた。

やりたいこと自体はそんなに複雑じゃない。

定価があって、

  • 値引率を入力したら値引額を出す
  • 値引額を入力したら値引率を逆算する

というだけ。

たとえば定価10万円で10%引きなら、値引額は1万円。

逆に値引額が1万円なら、値引率は10%。

まあ、普通の計算やな。

実装方法だけ確認したい場合はこちら JavaScriptで金額計算するときの浮動小数点対策

で、こういう細かい処理はAIに相談しながら書くと早いので、既存コードを渡して修正してもらった。

最初に出てきた「率 → 額」の計算はざっくりこんな感じだった。

const rate = numValue / 100;
const discounted = Math.floor(listPrice * (1 - rate));
const discountAmount = listPrice - discounted;

割引後の税抜価格を出して、小数点以下を切り捨てる。

そこから定価との差額を値引額として表示する。

考え方としてはやりたいことと合ってる。

よしよし。

……いや、待てよ。

金額計算で普通に小数使ってるけど大丈夫なん?#

ここで急に嫌な予感がした。

JavaScriptの浮動小数点。

存在自体は知ってた。

有名なのはこれ。

0.1 + 0.2 === 0.3

これがfalseになるやつ。

「はいはい、コンピュータでは小数を完全には表現できないことがあるんやろ」

くらいの認識はあった。

でも今書いてるの、見積システムやぞ。

listPrice * (1 - rate)

とか普通にやっとるやんけ。

しかもその直後に、

Math.floor(...)

してる。

浮動小数点の誤差と切り捨て。

この2人、隣の席に座らせて大丈夫な組み合わせなんか?

気になったのでAIに聞いてみた。

「なんか浮動小数点とかなんとかを考慮せなあかんのちゃうの?」

すると、

考慮した方がいい。金額や率は整数化して計算した方が安全。

という回答が返ってきた。

だよね。

絶対考慮した方がいいよね。

……。

待てやおい。

んなら最初から考慮して入れとかんかい!

何をしれっと「ああ、当然そうだけど?」みたいに言うてんねん!

お前が最初に出してきたコードには、それ入ってなかったんやぞ!

まず謝れやコラ!

……ということをAIに言っても仕方ないので、いったん口角を上げた状態を保ちつつ、コードを直してもらうことにした。

率も整数にして計算する#

値引率は小数1桁まで扱う想定だった。

たとえば、

7.3%

なら、10倍して

73

として扱う。

AIが出してきたコードもそんな方向だった。

const rate_x10 = Math.round(numValue * 10);

7.3%なら73。

ここまでは分かる。

そして、割引後価格を求めるための倍率を1000倍スケールで作る。

本来、

7.3% = 0.073 = 73 / 1000

なので、

1 - 0.073
= 927 / 1000

になる。

つまり、

const multiplier_x1000 = 1000 - rate_x10;

でいい。

ところが、AIが「対策版です」と出してきたコードはこうだった。

const multiplier_x1000 = 1000 - rate_x10 * 10;

ん?

その* 10、どっから来た。

7.3%なら、

rate_x10 = 73

なのに、さらに10倍して730を引いてしまう。

1000 - 730 = 270

つまり7.3%引きのつもりが、倍率0.27。

73%引きになっとるやないか。

浮動小数点の1円誤差を警戒していたら、割引率が10倍になって帰ってきた。

警備員呼んだら警備員が商品持って走っていったくらい話が違う。

「対策版です」と言われると余計に怖い#

ここが今回一番ゾッとしたところだった。

最初のコードならまだ疑える。

「小数使ってるけど大丈夫か?」

と、自分で引っかかった。

でもそのあと、

「浮動小数点を考慮した安全なコード」

として出てきたものは、心理的にはちょっと信用してしまう。

こっちは一度問題を指摘してるから、

じゃあ今度は対策されたんやな

と思いやすい。

でも普通に間違ってた。

しかも構文エラーでもない。

コードとしては動く。

変数名もそれっぽい。

コメントまで付いてる。

const multiplier_x1000 = 1000 - rate_x10 * 10;

これだけ見せられたら、流れでコピペしてしまう人もいると思う。

今回たまたま、

「いや、rate_x10ってもう百分率を10倍した値やん」

と気づいたから止められた。

気づかなかったら?

普通に間違った値引額を表示するシステムが出来上がる。

AIが出したから大丈夫、が一番危ない#

ここで浮動小数点とは別の怖さが出てきた。

AIを使ってコードを書くこと自体はめちゃくちゃ便利。

俺も使ってる。

既存コードを渡して、

「ここをこう変えたい」

と頼めば、叩き台を作る速度はかなり上がる。

でも、

AIが出したコードを採用できることと、そのコードが正しいと判断できることは別。

今回の例なら、

7.3% → 73

にした理由を理解していれば、

1000 - 73 * 10

を見たときに、

「いや待て」

となる。

逆に、

「なんか浮動小数点対策で整数化してるらしい」

くらいの理解でコピペしていたら、そのまま通る可能性がある。

AIがもっともらしい変数名とコメント付きで出してくるぶん、下手すると自分で雑に書いたコードより信用してしまう。

そこが怖い。

消費税の計算も気になってきた#

一度気になり始めると、他の計算も全部怪しく見えてくる。

実際、見積システムでは消費税も計算していた。

こんなコード。

tax = Math.floor(finalTotal * Const.TAX * 0.01);

税率10%なら、

finalTotal * 10 * 0.01

という計算。

これも0.01という小数をわざわざ使っている。

だったら少なくとも、

tax = Math.floor(finalTotal * Const.TAX / 100);

のように、税率を整数として扱った方が考えやすい。

もちろんJavaScriptのNumber自体には安全に整数として扱える範囲があるので、「整数にしたら宇宙の果てまで絶対安全」という話ではない。

ただ、今回扱う通常の見積金額の範囲なら、金額と率を整数スケールで扱う方が、小数を何となく掛け回すより意図も追いやすい。

そして何より、

どこで丸めるかを仕様として決める。

これが大事だった。

「最後に何となくfloorしとけばええやろ」ではなく、

  • 割引後価格を確定するとき
  • 消費税額を確定するとき
  • 値引額を表示するとき

それぞれ何を元に計算して、どこで端数処理するのかを決めておく。

金額計算になると、コードの問題だけじゃなく仕様の問題にもなる。

PythonならDecimalがある。じゃあ安心?#

バックエンドはDjangoなので、Python側の計算も気になった。

そこで出てきたのがDecimal。

from decimal import Decimal

小数を正確に扱いたいときによく使うやつ。

ここでも一個注意がある。

Decimal(0.01)

と、

Decimal("0.01")

は同じ気持ちで書かない方がいい。

前者は一度0.01をfloatとして作ってからDecimalに渡す。

後者は文字列の"0.01"から直接Decimalを作る。

金額計算でDecimalを使うなら、最初からDecimalとして正確な値を作る方が分かりやすい。

この辺まで来ると、

小数、お前ちょっと座れ。

という気分になってくる。

しかもAIの説明自体もレビュー対象だった#

今回さらに面白かったのが、コードだけじゃない。

AIから、

「math.floor()はDecimalをfloatに変換するから危険」

という趣旨の説明も途中で出てきた。

でも、こういう説明も含めてそのまま記事にはできない。

コードだけレビューして説明文は信用する、では意味がない。

AIが生成したものなら、

  • コード
  • 数式
  • APIの挙動
  • 「これは安全」という断言

全部が確認対象になる。

今回の記事を書くにあたっても、会話中に出てきた説明をそのまま「正解集」として並べるのではなく、実際に確認できたことと、改めて技術検証が必要なことを分けることにした。

「AIの回答を鵜呑みにするな」という記事でAIの回答を鵜呑みにしたら、さすがに完成度の高すぎるオチになってしまう。

今なら最初に何を見るか#

今回みたいな金額計算を書くなら、今なら最初にこの辺を確認する。

まず、

その値は金額なのか、率なのか。

次に、

小数を本当に持つ必要があるのか。

率が小数1桁までなら、7.3%を73として扱える。

そして、

どの時点で端数処理するのか。

割引後金額を切り捨てるのか、値引額を切り捨てるのか。

消費税は何に対して計算するのか。

ここが曖昧なままコードを書き始めると、計算式だけ正しくても最終結果が仕様と合わなくなる。

最後に、

具体的な数字を入れて自分で検算する。

今回の* 10なんて、7.3%を実際に入れてみればすぐ分かる。

AIにコードを書かせる場合、この最後の工程はむしろ前より重要になった気がする。

AIにコードを書かせるな、ではない#

今回の件で、

「AIにコードを書かせるのは危険だからやめよう」

とは思ってない。

むしろ便利なのでこれからも使う。

ただ、

AIがコードを書けることと、自分がそのコードを理解しなくていいことは全然つながってない。

AIが10分かかるコードを1分で出してくれたとしても、その1分後に、

「これ何してる?」

と聞かれて説明できないなら、まだ採用する段階じゃない。

今回も浮動小数点に気づかなければ、最初のコードをそのまま使っていたかもしれない。

浮動小数点に気づいたあとも、* 10に気づかなければ、今度はもっと派手に間違ったコードを使っていたかもしれない。

AIにレビューを頼んで安心したら、そのレビュー結果にもレビューが必要だった。

なんかもうレビューのマトリョーシカやな。

便利になったはずなのに、最後に必要なのは結局、

これ、ほんまに合ってる?

と一回疑うことだった。

金額計算では特に、その一回をサボらん方がよさそうだ。