Slide 1

Slide 1 text

PHPで任意精度演算を行って 
 「正しい」金額計算をする方法 2023年3月24日 / PHPerKaigi 2023 • Day 1 合同会社テンマド 山岡広幸

Slide 2

Slide 2 text

さて、突然ですが

Slide 3

Slide 3 text

0.1 + 0.2 = ?? php > var_dump(0.1 + 0.2);

Slide 4

Slide 4 text

0.1 + 0.2 ≠ 0.3 php > var_dump(0.1 + 0.2); float(0.30000000000000004) 🤔

Slide 5

Slide 5 text

自己紹介 - 山岡広幸 / Twitter: @hiro_y - Webアプリケーションエンジニア - PHPは3から。最近はNode.js多め - 合同会社テンマド 代表社員

Slide 6

Slide 6 text

- 受託制作・コンサルティング - 各種アドバイザー、各種制作(デザイン・開発) - 例: 株式会社イノベーター・ジャパン 社外CTO - 自社サービスの運営 - iruca、mimemoなど 👨💻👨💻👩💻👩💻

Slide 7

Slide 7 text

0.1 + 0.2 が 0.3 にならない世界

Slide 8

Slide 8 text

我々は PHP 8 の 時代を生きているので

Slide 9

Slide 9 text

型で考えてみる

Slide 10

Slide 10 text

0.1の型? php > var_dump(0.1); float(0.1) php > var_dump(gettype(0.1)); string(6) "double" 🧐

Slide 11

Slide 11 text

マニュアルを見る https://www.php.net/manual/ja/language.types. fl oat.php

Slide 12

Slide 12 text

fl oat / double - PHPで、整数は int(integer)という型 - 10進数、16進数、8進数、2進数で指定可能 - 「整数以外」を表現するのに浮動小数点数を使う - fl oat(ただし倍精度)、double - intの上限(PHP_INT_MAX)を超える整数も対象

Slide 13

Slide 13 text

浮動小数点数 もう少しくわしく

Slide 14

Slide 14 text

2進数の世界 - コンピューターは2進数で動いている - 0 or 1(OFF or ON)のバイナリの世界 - 10進数の小数は2進数の世界で表現可能か? - 0.1(2進数)は2-1(10進数の0.5) 
 0.01(2進数)は2-2(10進数の0.25)

Slide 15

Slide 15 text

翻って、我々の世界 - 10進数で物事を考えている - 部分的に他の基数も使うが、10進数が基本 - 2進数をそのまま扱うのは不便すぎる - 10進数の0.1は2進数で正確に表現できない

Slide 16

Slide 16 text

浮動小数点数 - 10進数の実数を有限桁の2進数の近似値で表現 - 浮動小数点( fl oating-point) - -1 x 0.101010101010101 x 2-1 と 
 -1 x 1.01010101010101 x 2-2 は同じ 
 符号、仮数、指数 - 複数フォーマットがある(そのうちの1つがIEEE 754)

Slide 17

Slide 17 text

不便すぎるので - 必要な桁数の精度を得るための演算処理を実装 - ただし、コストがかかるので非常に遅くなる… - そのうちの1つの方式が「任意精度演算」 - 多くの環境で実装されている

Slide 18

Slide 18 text

ここで体験談

Slide 19

Slide 19 text

新卒のころの話 - 新卒研修はCOBOL(その後Java) - 金融系のプロジェクトに配属 - 投資信託/ファンドのパフォーマンス評価 - たくさん小数点計算が必要だった

Slide 20

Slide 20 text

計算を何気なく実装 result = value * rate; 😡

Slide 21

Slide 21 text

Javaで十進演算するには - java.lang.BigDecimalを使う必要がある - プリミティブな型の fl oatを使ってはいけない - COBOLは10進数を言語でサポート(二進化十進数) - C#ならdecimal、RubyならBigDecimal

Slide 22

Slide 22 text

我らがPHPの場合

Slide 23

Slide 23 text

言語として 任意精度計算の 仕組みがない 😵

Slide 24

Slide 24 text

だけど大丈夫 https://www.php.net/manual/ja/language.types. fl oat.php

Slide 25

Slide 25 text

BC Math 関数 - GNU bc(Basic Calculator)からのfork - https://www.gnu.org/software/bc/ - PHPを「--enable-bcmath」を付けて構築 - 関数: bcadd、bccomp、bcdiv、bcmod、bcmul、 bcpow、bcpowmod、bcscale、bcsqrt、bcsub

Slide 26

Slide 26 text

例えば足し算 php > var_dump(bcadd('0.1', '0.2', 1)); string(3) "0.3" php > var_dump(bcadd('0.1', '0.2', 20)); string(22) "0.30000000000000000000"

Slide 27

Slide 27 text

GMP 関数 - GNU Multiple Precision Arithmetic Library - https://gmplib.org/ - PHPを「--with-gmp」を付けて構築 - とてもたくさんの数学関数群

Slide 28

Slide 28 text

雑なベンチマーク

Slide 29

Slide 29 text

高コストな結果 # php test.php float: 0.040148973464966ms bcadd: 0.86464095115662ms 🥺

Slide 30

Slide 30 text

注意: 引数は文字列で - BC Math関数に渡す引数は「文字列」で - fl oat表記で渡すとstringにキャストされる - 例: 0.00001 は 1.0E-5 になってしまう - 数値を表す文字列ではないのでエラーになる可能性

Slide 31

Slide 31 text

一応、もう一つのやり方 - PHPで整数の計算には誤差が発生しない - つまり、小数も整数に変換した上で計算、 
 小数に戻せば誤差は発生しない - 複雑になってしまうし、 
 絶対に間違えるのでオススメできない

Slide 32

Slide 32 text

【結論】 BC Math関数を使おう

Slide 33

Slide 33 text

ただし - BC Math関数に用意されているのは 
 基本的な算術関数のみであることに注意 - 例えば、 fl oorやceil、roundのような関数はない - 雑に fl oor関数等に渡すと fl oatに変換されるので 
 計算誤差が発生する可能性がある

Slide 34

Slide 34 text

便利ライブラリ紹介 - Brick\Math - https://github.com/brick/math - GMP関数、BC Math関数、手計算の各実装 - 四捨五入、切り捨て/切り上げが実装されている

Slide 35

Slide 35 text

Brick\Math例 $result = BigDecimal::of('2.5') ->multipliedBy(BigInteger::of('2')) ->divideBy('0.3', 3, RoundingMode::HALF_UP);

Slide 36

Slide 36 text

さて、余談 // Google Chrome DevTools Console > 0.1 + 0.2 0.30000000000000004

Slide 37

Slide 37 text

JavaScriptも同様 - 浮動小数点数、IEEE 754に準ずる - 解決方法: ライブラリを使う - https://github.com/MikeMcl/decimal.js - BigDecimalのプロポーザルあり - https://github.com/tc39/proposal-decimal

Slide 38

Slide 38 text

まとめ - 浮動小数点数の扱いには注意が必要 - BC Math関数を使えばOK - 必要に応じてライブラリを利用して楽をしよう - フロントエンド側も配慮が必要

Slide 39

Slide 39 text

実は身近な小数点計算 - 消費税、金利、住宅ローン…

Slide 40

Slide 40 text

正しい小数点計算を していくために

Slide 41

Slide 41 text

任意精度演算を 使っていこう (フロントもね!)

Slide 42

Slide 42 text

ありがとう ございました!