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

# 通常の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文字ずつ処理しているので遅そう

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

## 実装

```php
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進数に変換するサンプルです

```php
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 秒
```

# 実際に使ってみる

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

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

## 結果
```
680564733841876926926749214863536422910
```

使えそう？