おぎろぐはてブロ

なんだかんだエンジニアになって10年以上

加算子の挙動

これは知らなかった。

<?php
$a = 'Test';
echo ++$a . PHP_EOL;
$b = 'Test' + 1;
echo $a . PHP_EOL;

ちなみにこの結果は

Tesu
1
http://d.hatena.ne.jp/kidd-number5/20080112/1200067231
  • ++$a と、インクリメントすると、最下位の文字が t → u と、1文字ずれる
  • $b + 1 と、1 を加えると、0扱いされて、演算結果は 1 になる。

なんか違和感がある。

マニュアルみてみた。

PHP は、算術演算子で文字変数を扱った場合に C ではなく Perl の慣習に 従います。例えば、perl では 'Z'+1 は 'AA' を返しますが C では 'Z'+1 は '[' ( ord('Z') == 90, ord('[') == 91 ) を返します。 文字変数はインクリメントされることは可能ですがデクリメントは不可能であるということ、 またプレーンな ASCII 文字 (a-z および A-Z) のみがサポートされるということに注意しましょう。


Example#1 文字変数に対する算術演算子の使用

<?php
$i = 'W';
for ($n=0; $n<6; $n++) {
    echo ++$i . "\n";
}
?>

上の例の出力は以下となります。

X
Y
Z
AA
AB
AC
PHP: Incrementing/Decrementing Operators - Manual

PHP は、算術演算子で文字変数を扱った場合に、と始まるので、加算子とかまで含んだ算術演算全体について話しているように読み取れるけど(原文もそう)、インクリメントについての話。Perlだって、print 'Z'+1 は 1。
Perlは詳しくないけど、ここで指しているのは、マジカルインクリメントと呼ばれるものらしい。

インクリメント演算子には、ちょっと風変わりな機能が組み込まれています。数値が入った変数や、数値の文脈で使われてきた変数をインクリメントする場合には、通常のインクリメントとして動作します。しかし、その変数が設定されてからずっと文字列の文脈でしか使われていなくて、空でない /^[a-zA-Z]*[0-9]*$/ にマッチする値を持っているときには、個々の文字の範囲を保ちながら桁あげを行なって、文字列としてインクリメントが行なわれます (マジカルインクリメントと呼ばれます):

print ++($foo = '99');      # '100' と印字
print ++($foo = 'a0');      # 'a1' と印字
print ++($foo = 'Az');      # 'Ba' と印字
print ++($foo = 'zz');      # 'aaa' と印字
The Perl5 Manual - インクリメントとデクリメント

動き

このインクリメントの動きは、Zend/zend_operators.c の increment_function()の中で実装されていて、数値文字列でない場合(is_numeric_string()でLONGでもDOUBLEでもない)に、以下のように文字列インクリメントの処理がされます。

文字列の最下位から、以下の処理を行う。桁上げが起きた場合は、その次の文字で同じ処理を行う。

  • 'a' 〜 'y' の間のとき: 'a' → 'b' と次の文字に
  • 'z'のとき、'a' にして、桁上げ
  • 'A' 〜 'Y' の間のとき: 'A' → 'B' と次の文字に
  • 'Z'のとき、'A' にして、桁上げ
  • '0' 〜 '8' の間のとき: '0' → '1' と次の文字に
  • '9'のとき、'0' にして、桁上げ

桁上げを繰り返して、最後の文字(先頭の文字列)でも桁上げが起こった場合は、以下に従って、先頭に文字を追加する。

  • 最後の文字が数字(例: "9zz" + 1): '1'を付加
  • 最後の文字が英大文字(例: "Zzz" + 1): 'A'を付加
  • 最後の文字が英小文字(例: "z9Z" + 1): 'a'を付加