【Laravel】「えっ、数値じゃないじゃん!」decimal型が文字列になる理由
はじめに
Laravelで会計アプリや家計簿アプリなど「お金」を扱うアプリを作る際、DBのデータ型にdecimalを使う場面があると思います。
僕は、最初どうせ数値型(float型/int型/double型)の仲間だし、お金の計算はdecimal 使うみたいだからdecimalにしておけば安心ということでdecimalを実際に使いました。しかし、decimal型は数値として扱われないのです!
結論、decimal型は文字列として扱われます!
decimal型はなぜ、数値型ではなく string 型として返ってくるのか、その理由を解説していきます。
// DBのカラム定義: decimal(10, 2)
$product = Product::find(1);
var_dump($product->price);
// string(7) "1000.00" ← 文字列として扱われる!
この記事で分かること
- コンピューターが小数をどう表現しているか
- なぜ「きれいな10進数」でも2進数では正確に表せないのか
- decimal型が文字列として扱われる本当の理由
コンピューターは10進数の小数を正確に扱うのが苦手
前提として、私たちの世界では10進数が基数ですが、コンピューターの世界では、2進数が基数です。
一方で10進数の小数を2進数に変換しようとすると、綺麗に計算が終わらないことが多いです。
具体例です。
10進数での小数表現
10進数では、小数点以下は「10のマイナス乗」で表現されます。
- 0.1 = 1 × 10⁻¹ (1/10)
- 0.01 = 1 × 10⁻² (1/100)
- 0.25 = 25 × 10⁻² (25/100 = 1/4)
2進数での小数表現
同じように、2進数の小数は**「2のマイナス乗の足し算」**で表現されます。
たとえば、2進数の 0.101 を10進数に変換すると:
0.101 (2進数)
= 1×2⁻¹ + 0×2⁻² + 1×2⁻³
= 1/2 + 0 + 1/8
= 0.5 + 0 + 0.125
= 0.625 (10進数)
つまり2進数の小数は、「1/2」「1/4」「1/8」「1/16」…の足し算でしか表現できません。
重要なポイント
10進数の計算と違い、2進数の小数は「2のべき乗の逆数(1/2, 1/4, 1/8...)の組合せ」でしか表せないという制約があります。
メモリの制約と近似値の問題
パソコンはメモリに計算結果を保存します。
2進数で0.1を表すと、「0.0001100110011…」のようになります。これは循環小数であり、無限小数です。
しかし、メモリは有限です。
つまり、メモリに限りがある以上、どこかで綺麗に計算が終わらない小数をなんとかして表現する必要があります。
IEEE 754による近似値表現
その解決策は近似値として扱うによる表現です。
この近似値を表現するための標準規格として、IEEE754が広く採用されてます。
var_dump(0.1 + 0.2);
//float(0.30000000000000004) ← 0.3にならない!
なぜdecimal型は文字列なのか
この誤差は、為替計算や会計処理などお金を扱う場面では致命的です。
Laravel(PHP)自体にDecimal型がないため、float型に変換された瞬間に誤差が出るに防ぐために
文字列として扱われます。
言い換えれば、decimal型を受け取る際に、精度を落とさないように文字列として保持する挙動をしてます。
まとめ
- コンピュータは2進数で計算するため、10進数の小数を正確に表現できないことがある
- メモリの制約により、無限小数は近似値として扱われる
- 近似値による誤差を避けるため、decimal型は文字列として扱われる
- お金の計算では、この文字列による正確性が重要
個人開発などで、計算をシンプルにしたい、かつ小数点以下を扱わないならint型を選択すべきだと考えます。個人的には今回の知見を通してほとんどの場合はint型がいいのではないかと実感しました。
Discussion