Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHPで任意精度演算を行って「正しい」金額計算をする方法 / Perform arbitrary precision arithmetic in PHP to achieve "accurate" monetary calculations

PHPで任意精度演算を行って「正しい」金額計算をする方法 / Perform arbitrary precision arithmetic in PHP to achieve "accurate" monetary calculations

YAMAOKA Hiroyuki

March 24, 2023
Tweet

More Decks by YAMAOKA Hiroyuki

Other Decks in Programming

Transcript

  1. PHPで任意精度演算を行って

    「正しい」金額計算をする方法
    2023年3月24日 / PHPerKaigi 2023 • Day 1


    合同会社テンマド 山岡広幸

    View Slide

  2. さて、突然ですが

    View Slide

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


    View Slide

  4. 0.1 + 0.2 ≠ 0.3
    php > var_dump(0.1 + 0.2);


    float(0.30000000000000004) 🤔

    View Slide

  5. 自己紹介
    - 山岡広幸 / Twitter: @hiro_y


    - Webアプリケーションエンジニア


    - PHPは3から。最近はNode.js多め


    - 合同会社テンマド 代表社員

    View Slide

  6. - 受託制作・コンサルティング


    - 各種アドバイザー、各種制作(デザイン・開発)


    - 例: 株式会社イノベーター・ジャパン 社外CTO


    - 自社サービスの運営


    - iruca、mimemoなど
    👨💻👨💻👩💻👩💻

    View Slide

  7. 0.1 + 0.2 が


    0.3 にならない世界

    View Slide

  8. 我々は PHP 8 の


    時代を生きているので

    View Slide

  9. 型で考えてみる

    View Slide

  10. 0.1の型?
    php > var_dump(0.1);


    float(0.1)


    php > var_dump(gettype(0.1));


    string(6) "double"
    🧐

    View Slide

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

    View Slide

  12. fl
    oat / double
    - PHPで、整数は int(integer)という型


    - 10進数、16進数、8進数、2進数で指定可能


    - 「整数以外」を表現するのに浮動小数点数を使う


    -
    fl
    oat(ただし倍精度)、double


    - intの上限(PHP_INT_MAX)を超える整数も対象

    View Slide

  13. 浮動小数点数


    もう少しくわしく

    View Slide

  14. 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)

    View Slide

  15. 翻って、我々の世界
    - 10進数で物事を考えている


    - 部分的に他の基数も使うが、10進数が基本


    - 2進数をそのまま扱うのは不便すぎる


    - 10進数の0.1は2進数で正確に表現できない

    View Slide

  16. 浮動小数点数
    - 10進数の実数を有限桁の2進数の近似値で表現


    - 浮動小数点(
    fl
    oating-point)


    - -1 x 0.101010101010101 x 2-1 と

    -1 x 1.01010101010101 x 2-2 は同じ

    符号、仮数、指数


    - 複数フォーマットがある(そのうちの1つがIEEE 754)

    View Slide

  17. 不便すぎるので
    - 必要な桁数の精度を得るための演算処理を実装


    - ただし、コストがかかるので非常に遅くなる…


    - そのうちの1つの方式が「任意精度演算」


    - 多くの環境で実装されている

    View Slide

  18. ここで体験談

    View Slide

  19. 新卒のころの話
    - 新卒研修はCOBOL(その後Java)


    - 金融系のプロジェクトに配属


    - 投資信託/ファンドのパフォーマンス評価


    - たくさん小数点計算が必要だった

    View Slide

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

    View Slide

  21. Javaで十進演算するには
    - java.lang.BigDecimalを使う必要がある


    - プリミティブな型の
    fl
    oatを使ってはいけない


    - COBOLは10進数を言語でサポート(二進化十進数)


    - C#ならdecimal、RubyならBigDecimal

    View Slide

  22. 我らがPHPの場合

    View Slide

  23. 言語として


    任意精度計算の


    仕組みがない 😵

    View Slide

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

    View Slide

  25. 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

    View Slide

  26. 例えば足し算
    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"


    View Slide

  27. GMP 関数
    - GNU Multiple Precision Arithmetic Library


    - https://gmplib.org/


    - PHPを「--with-gmp」を付けて構築


    - とてもたくさんの数学関数群

    View Slide

  28. 雑なベンチマーク


    $floatStart = microtime(true);


    for ($i = 0; $i < 10000000; $i++) {


    $result = 0.1 + 0.2;


    }


    $floatEnd = microtime(true);


    $bcStart = microtime(true);


    for ($i = 0; $i < 10000000; $i++) {


    $result = bcadd('0.1', '0.2', 1);


    }


    $bcEnd = microtime(true);


    echo 'float: ' . ($floatEnd - $floatStart) . "ms\n";


    echo 'bcadd: ' . ($bcEnd - $bcStart) . "ms\n";


    View Slide

  29. 高コストな結果
    # php test.php


    float: 0.040148973464966ms


    bcadd: 0.86464095115662ms
    🥺

    View Slide

  30. 注意: 引数は文字列で
    - BC Math関数に渡す引数は「文字列」で


    -
    fl
    oat表記で渡すとstringにキャストされる


    - 例: 0.00001 は 1.0E-5 になってしまう


    - 数値を表す文字列ではないのでエラーになる可能性

    View Slide

  31. 一応、もう一つのやり方
    - PHPで整数の計算には誤差が発生しない


    - つまり、小数も整数に変換した上で計算、

    小数に戻せば誤差は発生しない


    - 複雑になってしまうし、

    絶対に間違えるのでオススメできない

    View Slide

  32. 【結論】


    BC Math関数を使おう

    View Slide

  33. ただし
    - BC Math関数に用意されているのは

    基本的な算術関数のみであることに注意


    - 例えば、
    fl
    oorやceil、roundのような関数はない


    - 雑に
    fl
    oor関数等に渡すと
    fl
    oatに変換されるので

    計算誤差が発生する可能性がある

    View Slide

  34. 便利ライブラリ紹介
    - Brick\Math


    - https://github.com/brick/math


    - GMP関数、BC Math関数、手計算の各実装


    - 四捨五入、切り捨て/切り上げが実装されている

    View Slide

  35. Brick\Math例
    $result = BigDecimal::of('2.5')


    ->multipliedBy(BigInteger::of('2'))


    ->divideBy('0.3', 3, RoundingMode::HALF_UP);

    View Slide

  36. さて、余談
    // Google Chrome DevTools Console


    > 0.1 + 0.2


    0.30000000000000004

    View Slide

  37. JavaScriptも同様
    - 浮動小数点数、IEEE 754に準ずる


    - 解決方法: ライブラリを使う


    - https://github.com/MikeMcl/decimal.js


    - BigDecimalのプロポーザルあり


    - https://github.com/tc39/proposal-decimal

    View Slide

  38. まとめ
    - 浮動小数点数の扱いには注意が必要


    - BC Math関数を使えばOK


    - 必要に応じてライブラリを利用して楽をしよう


    - フロントエンド側も配慮が必要

    View Slide

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

    View Slide

  40. 正しい小数点計算を


    していくために

    View Slide

  41. 任意精度演算を


    使っていこう


    (フロントもね!)

    View Slide

  42. ありがとう


    ございました!

    View Slide