【PHP】進数変換関数のドキュメントにない挙動

  • 2020年11月10日
  • 2020年11月10日
  • PHP

 PHP には p 進数文字列を q 進数文字列に変換する関数 base_convert が用意されています。そしてよく行うであろう変換である 2進数<->10進数, 8進数<->10進数, 16進数<->10進数の変換には変換後が数値型になる関数である bindec, decbin, octdec, decoct, hexdec, dechex が用意されています。これら関数の内、base_convert の引数省略版であり n 進数を 10 進数に変換する関数である bindec, octdec, hexdec の説明は大体次の様な感じで引数で受け入れられる値に関してざっくばらんな説明となっています。

octal_string

変換したい 8 進文字列。
octal_string に無効な文字を与えても、静かに無視されます。
PHP 7.4.0 以降では、無効な文字を与えることは推奨されません。

PHP: octdec – Manual

 この 10 進数に変換する関数の引数文字列ですが、次の通りの挙動を見せます。
PHP Sandbox, test PHP online, PHP tester

<?php
echo '0b10  : '. 0b10 . "\n"; // 二進数表記数値
echo '010   : '. 010 . "\n"; // 八進数表記数値
// echo '0o10  : '. 0o10 . "\n"; // 八進数表記の o は文法エラーになります
echo '--ここから bindec--'."\n";
echo "10    : ".bindec(10). "\n"; // 10 を文字列にした後に二進数として解釈されて 2 が出力
echo "13    : ".bindec(13). "\n"; // 3 が二進数として解釈できずに非推奨警告発生
echo "'10'  : ".bindec('10'). "\n"; // 文字列である'10'を二進数として解釈して 2 が出力
echo "'010' : ".bindec('010'). "\n"; // '010'の0を読み飛ばして残りの'10'を二進数として解釈して 2 が出力
echo "'0010': ".bindec('0010'). "\n"; // 0 が連続しても全て呼び飛ばして 2 が出力
echo "'0b10': ".bindec('0b10'). "\n"; // 0b を二進数を示す前置詞と解釈して飛ばし、残りの'10'を二進数として解釈して 2 が出力
echo "'0B10': ".bindec('0B10'). "\n"; // 0B を二進数を示す前置詞と解釈して飛ばし、残りの'10'を二進数として解釈して 2 が出力
echo 'true  : '.bindec(true). "\n";// true を文字列変換して'1'として'1'を二進数と解釈して 1 が出力
echo 'false : '.bindec(false). "\n";// false を文字列変換して''として''を処理して 0 が出力
echo 'null  : '.bindec(null). "\n";// null を文字列変換して''として''を処理して 0 が出力
echo "''    : ".bindec(''). "\n";// 0 が出力
echo "' '   : ".bindec(' '). "\n";// 空白文字を削って''を処理して0が出力
echo '" \t\n\f\v101 \n": '.bindec(" \t\n\f\v101 \n"). "\n";// 端の空白文字を削って'101'を処理して5が出力
echo '--ここから octdec--'."\n";
echo 'octdec("0o10"): '.octdec("0o10")."\n";// 0o を八進数を示す前置詞と解釈して飛ばし、残りの'10'を八進数として解釈して 10 が出力

/** 出力結果
0b10  : 2
010   : 8
--ここから bindec--
10    : 2
<br />
<b>Deprecated</b>:  Invalid characters passed for attempted conversion, these have been ignored in <b>[...][...]</b> on line <b>7</b><br />
13    : 1
'10'  : 2
'010' : 2
'0010': 2
'0b10': 2
'0B10': 2
true  : 1
false : 0
null  : 0
''    : 0
' '   : 0
" \t\n\f\v101 \n": 5
--ここから octdec--
octdec("0o10"): 8
*/

 この記事を書くきっかけになった 8 進数の話もさることながら、数値と空白と空文字列の扱いが奇妙な感じがします。
PHP: rfc:explicit_octal_notation
8進数も基数表示を明記したい – Qiita
 数値や true の扱いはこれらを文字列変換した時の挙動を知っていればなぜ上述の様な結果になるか想像がつきます。しかし一方で空白と空文字列と空文字列に変換されるfalse, null が謎です。0 が返ってきますが、エラーになってもそう違和感を感じません。実際、他言語で次の様に二進数文字列→十進数数値の変換を行おうとすると数値として不適切な文字列であるとわかる結果になります。

// 手元で実行できた適当なバージョンの実行結果なのであくまで参考程度です
// JavaScript
parseInt('',2) // NaN 返却
// Python
int('',2) // ValueError 発生。Python 2, 3 問わず
// Go
strconv.ParseInt(str, 2, 0) // parsing "": invalid syntax でエラー発生
// Java
Integer.parseInt('', 2); // NumberFormatException 発生
// MySQL
CONV(BINARY (''), 2, 10) // null 返却

 PHP の 0 返却の挙動は PHP の実装で作られています(PHP 自体のテストに false や null を渡したら 0 が返ってくる、というテストがあるので挙動の変更はほぼ確実に気付かれ、まず間違いなくアナウンスされます)。
 これらの関数の実装は ext/standard/math.c 中の _php_math_basetozval 関数で行われています。
php-src/math.c at fa5a25b8bbe502916f28d6fa60118241714e0c79 · php/php-src
 この関数の実装は簡単に(エラー処理、数値のサイズの処理を飛ばして)言えば次の様になっています。

  1. 初期値 0 の返り値用変数を用意
  2. 引数の文字列の前後の空白を除去(実際は文字列を読み込むカーソルの位置合わせをしています)
  3. 前後の空白を除去した文字列内の数値を順序読みこんで返り値変数に加算
  4. 文字列を処理しきったら返り値用変数を返却

 引数の文字列が全て空白系の場合、3 の処理が一切行われないため初期値である 0 が返ってきます。

 この様に PHP の進数変換関数は昔ながらの暗黙処理が残っています。昨今の PHP は型付け等の大規模プログラミング向けの厳密な挙動が増えてきていますし、その内これも空文字列を渡したら Warning 発生といった具合に厳格化されるやもしれません。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG