2024年3月7日~9日、PHPerKaigi 2024が開催されました。
私はオンライン参加勢だったのですが、メインであるセッション・LT以外に、PHPerチャレンジやコードゴルフといった企画も用意されていて非常に楽しませてもらいました。
コードゴルフは、与えられた仕様を満たすコードをいかに短く実装できるかを競うゲームで、今回は3問出題されました。
これが楽しかったのでいろいろやっていたところ、第2問、第3問で1位を取ってしまいました。
せっかくなので、どんなコードを書いたのか紹介しておきます。
なお、問題および私を含めた回答者のコードはこちらで公開されています。
https://fortee.jp/phperkaigi-2024/go/golf
第2問:Base32
Base64ならぬBase32の変換を実装するという問題です。
そして、私のコードがこちらです。
while($l=fgets(STDIN)){$b='';for($i=0;$i<strlen($l)-1;)$b.=sprintf('%08b',ord($l[$i++]));foreach(str_split($b,5)as$j)echo[...range('A','Z'),...range(2,7)][bindec(str_pad($j,5,0))];echo str_repeat('=',[0,6,4,3,1][$i%5]),"\n";}
さすがに何を書いてるかさっぱりだと思うので、改行やインデントを挿入すると以下のようになります。
while ($l = fgets(STDIN)) {
$b = '';
for ($i = 0; $i < strlen($l) - 1;)
$b .= sprintf('%08b', ord($l[$i++]));
foreach (str_split($b, 5) as $j)
echo [...range('A', 'Z'), ...range(2, 7)][bindec(str_pad($j, 5, 0))];
echo str_repeat('=', [0, 6, 4, 3, 1][$i % 5]), "\n";
}
これは問題ページにある実装例を変形していった結果出来上がったものです。どのように変形したのか、以下に紹介していきます。
配列の短縮
実装例を見ると、TABLE
配列が明らかに文字数を食っています。
連番の配列といえばrange
関数ですが、数値の連番だけでなく、文字列の連番も生成することができます。
ということで、TABLE
は以下のように短縮できます。
const TABLE = [...range('A', 'Z'), ...range('2', '7')];
変数、定数の削減
上でTABLE
定数の定義を短くしましたが、そもそもこの定数は1箇所でしか使われていないので、定数を定義せずに配列リテラルをそのまま埋め込むと文字数が削れます。
- const TABLE = [...range('A', 'Z'), ...range('2', '7')];
// ...
- $base32 .= TABLE[base_convert(str_pad($b, 5, '0'), 2, 10)];
+ $base32 .= [...range('A', 'Z'), ...range('2', '7')][base_convert(str_pad($b, 5, '0'), 2, 10)];
また、PHP_EOL
は"\n"
に置き換えました。他の人の回答では、\n
の2文字の代わりに改行そのもの (LF) を文字列に入れている人も多かったですね。
さらに、エンコード結果を$base32
変数に格納せず、echoで標準出力に都度出すだけにすると、$base32
変数が完全に不要になります。
foreach (str_split($bits, 5) as $b) {
echo [...range('A', 'Z'), ...range('2', '7')][base_convert(str_pad($b, 5, '0'), 2, 10)];
}
echo match (strlen($bits) % 40) {
8 => '======',
16 => '====',
24 => '===',
32 => '=',
default => '',
}, "\n";
類似の関数の利用
base_convert
は任意のn進数→m進数 (2<=n,m<=36) の変換ができる便利な関数です。
ただ、2進数⇔10進数の変換は頻繁に使われることもあってか、専用の関数が用意されている1ため、置き換えることができます。
さらに、for文の中では4つもの関数が呼ばれていますが、何をやっているか丁寧に見ると、
- 入力中の1文字を取得
- ASCIIコードに変換
- 2進数に変換
- 0埋め8桁の文字列に変換
という処理になっています。
このうち「2進数に変換 + 0埋め8桁の文字列に変換」の部分は、sprintf
を使えば一発です。
- $bits .= str_pad(base_convert(strval(ord($line[$i])), 10, 2), 8, '0', STR_PAD_LEFT);
+ $bits .= sprintf('%08b', ord($line[$i]))
暗黙の型変換の利用
str_pad
の第3引数はstring型ですが、今回declare(strict_types=1)
は付いていないので、数値の0
を渡しても文字列の"0"
に変換されます。
またechoで出力する値も数値→文字列に変換されるので、シングルクォーテーションをいくつか削ることができます。
- echo [...range('A', 'Z'), ...range('2', '7')][bindec(str_pad($b, 5, '0'))];
+ echo [...range('A', 'Z'), ...range(2, 7)][bindec(str_pad($b, 5, 0))];
後から気づきましたが、ここまで来ると...range(2,7)
より2,3,4,5,6,7
の方が短かったですね。
rtrim
をやめる
fgets
で読み込んだ結果から、末尾の改行を取るためにrtrim
を呼んでいました。
今回に関しては、入力文字列はforで1文字ずつループするだけなので、改行を取るのをやめて代わりにループ回数をstrlen($line) - 1
にしました。
厳密には、改行コードがCRLFの2バイトだったり改行無しで入力が終わったりしていたら成り立たないのですが、今回のテストケースにはそのようなパターンはないようでした。
=
パディングのmatchを削る
実装例では、strlen($bits) % 40
の結果に応じて末尾に=
のパディングが0~6文字入るようになっています。
$bits .= sprintf('%08b', ord($line[$i]))
先程$bits .= sprintf('%08b', ord($line[$i]))
というコードがあった通り、$bits
の文字数は8の倍数になります。
割る数の40も8の倍数なので、全体を8で割って、以下のように変形できます。
match ((strlen($bits) / 8) % 5) {
1 => '======',
2 => '====',
3 => '===',
4 => '=',
0 => '',
}
すると、=>
の左側が0~4の連番なので、配列を使って書き換えられます。
['', '======', '====', '===', '='][(strlen($bits) / 8) % 5]
同じ文字の繰り返しなので、str_repeat
を利用できます。
str_repeat('=', [0, 6, 4, 3, 1][(strlen($bits) / 8) % 5])
ここで、strlen($bits) / 8
の部分に注目すると、これは最初に$bits
文字列を組み立てるときのfor文のループ回数と同じです。
PHPではループが終了した後もループ変数はそのまま生き残るので、strlen($bits) / 8 === $i
です。
最終的に、match式の部分は以下のように書けます。
str_repeat('=', [0, 6, 4, 3, 1][$i % 5])
変数の参照とインクリメントをまとめる
通常for文のカッコの最後にあるインクリメントを、ループの中の$i
を使っている部分で一緒にやってしまいます。
普段はあまり意識しないであろう前置インクリメントと後置インクリメントの違いを考えて使う必要があります。
- for ($i = 0; $i < strlen($line) - 1; $i++) {
- $bits .= sprintf('%08b', ord($line[$i]));
+ for ($i = 0; $i < strlen($line) - 1;) {
+ $bits .= sprintf('%08b', ord($line[$i++]));
}
あとは細かい修正をすれば冒頭のスクリプトとなります。
- 変数の1文字化
- 冗長な波括弧の削除
最後に
本当は第3問も書こうと思ったんですが、長くなったので一旦ここまでにします。
他の方の回答も見てみましたが、以下2点をやっている人はあまりいなさそうでした。
-
strlen($bits) / 8
の代わりに$i
を使う -
rtrim()
をやめる
関数呼び出しはそれなりに文字数を食うので減らせると大きいですね。
-
8進数⇔10進数、16進数⇔10進数の変換も同様の関数があります ↩