通常の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進数を使うには事前に変換が必要
-> 変換処理を実装して解決
変換方法
ググれば出てくる
- https://www.php.net/manual/ja/function.hexdec.php#90309
- https://pgmemo.tokyo/data/archives/1270.html
でも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
使えそう?