LoginSignup
1
0

More than 3 years have passed since last update.

PHPのBC Mathで16進数を利用する方法

Last updated at Posted at 2020-07-18

通常の16進数 -> 10進数

PHPでは hexdec() を使うことで16進数文字列を10進数(int)に変換できます
https://www.php.net/manual/ja/function.hexdec.php

BC Math

  • Javaで言うBigDecimal
  • 文字列を使って計算する誤差が出ないやつ
  • 誤差が出てはいけない大きい桁の金額とかを計算するときはこれを使おう
  • https://www.php.net/manual/ja/ref.bc.php

問題点

BC Math関数の引数は全て10進数表現の文字列しか受け取れないため、16進数を使うには事前に変換が必要

-> 変換処理を実装して解決

変換方法

ググれば出てくる

でも1文字ずつ処理しているので遅そう

-> まとめて処理すれば速くなる?

実装

function bcHexDec(string $value): string {
    $value = str_replace('0x', '', $value);
    if (!ctype_xdigit($value)) {
        throw new InvalidArgumentException('Invalid hex');
    }

    $dec = hexdec($value);

    // hexdecの結果がintの上限を超えていない場合はそのまま返す
    if (is_int($dec)) {
        return (string)$dec;
    }

    // hexdecの結果がintの上限を超えた場合はBC Mathを使い処理する

    // PHPはunsignedに非対応のためPHP_INT_SIZE * 2ではオーバーフローすることがある
    // よって-2することで32bit環境では3byte、64bit環境では7byteずつに区切って処理し上限を超えないようにする
    $splitSize = PHP_INT_SIZE * 2 - 2;
    $valueLength = strlen($value);
    $valueArray = str_split(
        // 入力されたhexの桁数が区切りサイズの倍数でない場合、0埋めして合わせる
        $valueLength % $splitSize > 0 ?
            str_pad($value, $splitSize * ((int)($valueLength / $splitSize) + 1), '0', STR_PAD_LEFT) :
            $value,
        $splitSize
    );
    $dec = '0';
    bcscale(0);
    foreach ($valueArray as $value) {
        // $dec = $dec * (16 ** $splitSize) + hexdec($value);
        $dec = bcadd(bcmul($dec, (string)(16 ** $splitSize)), (string)hexdec($value));
    }

    return $dec;
}

重要な箇所は PHP_INT_SIZE * 2 - 2 の部分

PHP_INT_SIZE は実行しているPHPで使用可能なintのサイズを取得します

32bit環境では 4 、64bit環境では 8 が返ります

このサイズをフルに使った場合、32bit環境を例にすると

0xFFFFFFFF が上限になるはずですが、PHPはunsignedに対応していません

よって、 0x7FFFFFFF が上限となってしまい、この値を超えた場合はPHPの仕様によりfloatとして扱われてしまいます

つまりPHP_INT_SIZEのサイズで区切った場合はオーバーフローする可能性があります

そこで上限サイズより1byte小さいサイズで区切ることでオーバーフロー対策をしつつ、1文字ずつ処理するよりは速く動かせるはずです

本当に速いか?

以下の検証コードを用意しました
128bit上限値の16進数を10進数に変換するサンプルです

function my_bcHexDec(string $value): string {
    // 上に同じコードがあるので省略
}

function bchexdec($hex) {
    $len = strlen($hex);
    $dec = '0';
    for ($i = 1; $i <= $len; $i++)
        $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));

    return $dec;
}


// 今回実装したもの
$startTime = microtime(true);
for ($i = 0; $i < 100000; $i++) {
    $res = '';
    $res = my_bcHexDec('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF');
    if ($res !== '340282366920938463463374607431768211455') {
        exit(1);
    }
}
$time = microtime(true) - $startTime;
echo "{$time}\n";

// ネットに転がっているサンプル
$startTime = microtime(true);
for ($i = 0; $i < 100000; $i++) {
    $res = '';
    $res = bchexdec('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF');
    if ($res !== '340282366920938463463374607431768211455') {
        exit(1);
    }
}
$time = microtime(true) - $startTime;
echo "{$time}\n";

結果

10倍以上速いですね
検証は64bit環境で行っており、32bit環境だと少し遅くなるかもしれません

0.40806603431702 秒
4.6333608627319 秒

実際に使ってみる

function bcHexDec(string $value): string {
    // 上に同じコードがあるので省略
}

echo bcadd(bcHexDec('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'), bcHexDec('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'));

結果

680564733841876926926749214863536422910

使えそう?

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0