PHPerコードバトルで書いたコードを紹介してみる

先日 PHPerKaigi 2025 に参加し、PHPer コードバトルで優勝させていただきました。

takaram.hatenablog.com

PHPer コードバトルで僕がどんなコードを書いてたか、というのを紹介していくだけの記事です。

準決勝 第1試合

問題はこちら https://t.nil.ninja/phperkaigi/2025/code-battle/golf/11/watch

標準入力から行区切りでテキストが入力されます。テキストに含まれる数字を漢数字に置換して出力してください。置換は一桁ずつおこない、数字が連続している場合でも「十」や「百」にはしないでください。

入力:
1234567890
第8回
ペチパー会議2025
気温は摂氏7度
総勢123人
税込98700円

出力:
一二三四五六七八九〇
第八回
ペチパー会議二〇二五
気温は摂氏七度
総勢一二三人
税込九八七〇〇円

本戦の15分間で僕が書いたコードはこちら

<?php
while($l = fgets(STDIN))
    echo strtr(
        $l,
        str_split('〇一二三四五六七八九', 3)
    );

練習がてらやっていたオンライン予選で見つけたstrtrを利用したコードです。

www.php.net

strtrは、たとえば "a → A, b → B, c → C" のような変換をするときに2通りの書き方ができます。

<?php
// パターン1
strtr($str, 'abc', 'ABC');
// パターン2
strtr($str, ['a' => 'A', 'b' => 'B', 'c' => 'C']);

この2つ目の書き方を利用しています。ちょうど置換前の文字が0からの連番なので、'0' =>のようにキーを書く必要がありませんでした。

また、配列は['〇', '一', '二', ...]の代わりにstr_splitを使っています。普通はmb_str_splitを使うところですが、全て3バイト文字なのでstr_splitの第2引数を指定して3バイトずつ区切っています。

試合後解説のめもりーさんがおっしゃっていたように、1行ずつ処理せずstrtrの第1引数に直接fread(STDIN, 1e5)とかを渡すともっと短くなります。

準決勝 第2試合

問題はこちら https://t.nil.ninja/phperkaigi/2025/code-battle/golf/12/watch

標準入力から「西暦年-月」の形式で 2025-02 のような行が1行入力されます。入力された月に合わせて「[Y年M月]」と出力してから、入力された月のカレンダーを日曜始まり形式で整列して出力してください。日付はすべて「3桁空白右寄せ」で表示します。

入力:
2025-02

出力
[2025年2月]
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28

はてなブログだと出力例部分が表示崩れしてしまうようです

こちらは自分は出場していませんが、観戦しながら自分でも書いていたコードを紹介します。

<?php
$f = strtotime(fgets(STDIN) . "-1");
echo date("[Y年n月]\n", $f);

foreach(
    array_chunk([
        ...array_fill(0, date('w', $f), ''),
        ...range(1, date('d', strtotime("last day of", $f)))
    ], 7) as $w
){
    foreach ($w as $d)
        printf("%3s", $d);

    echo "\n";
}

出力用の配列を作るために、サンプルコードではもう1つforeachループがありましたが、array_chunkを使うことでそれが不要になっています。

www.php.net

array_chunkを使うのは、作問担当の@tadsanさんの想定解だったようです。

後から知ったんですが、date('d', strtotime("last day of", $f))の部分はdate('t', $f)でよかったようです。何ですかtって……

www.php.net

t 指定した月の日数。 28 から 31

決勝

問題はこちら https://t.nil.ninja/phperkaigi/2025/code-battle/golf/13/watch

標準入力の最初の行に出力の最大行数 $max が、次以降の行に置換リストが 数,名前 の形式で改行区切りで入力されます。置換リストに含まれる数は2以上の素数で、昇順に入力されます。1 から $max までの連続した整数を改行区切りで出力してください。その際、出力しようとしている数が置換リストの数の倍数なら対応する名前に置き換え、その数が置換リストに含まれる複数の数の公倍数ならリスト内の数が小さい順に連結して置き換えてください。

入力:
10
2,Dizz
3,Fizz
5,Buzz

出力:
1
Dizz
Fizz
Dizz
Buzz
DizzFizz
7
Dizz
Fizz
DizzBuzz

決勝の15分で書いたコードがこちら

<?php
$x = fgets(STDIN);
while ([$n, $m] = fgetcsv(STDIN)) {
    $a[$n] = $m;
}

for ($i = 0; $i++ < $x; $s = '') {
    foreach ($a as $n => $m)
        $i % $n || $s .= $m;

    echo $s ?: $i, "
";
}

元のサンプルコードから離れることなく、地道に短縮していった結果です。

最終日にランキングを見てみたら、上のコード以上に短縮している方がたくさんいらっしゃったので、自分もさらにやってみた結果↓のようになりました。

<?php
for ($x = fgets(STDIN); !$s = ++$i > $x; $a[] = fgetcsv(STDIN)) {
    foreach ($a as $r)
        $r && $s .= $r[$i % $r[0] + 1];

    echo $s ?: $i, "
";
}

入力を読み込むループと出力するループを1つにまとめてしまっています。

1, 2, 3, ... と順番に出力するわけですが、nを出力する段階では、n以下の置換リストだけ読み込めていれば十分です。 問題の制約として、置換リストの入力は「2以上の素数で昇順」というのを考慮すると「1行出力して1行読み込む」で問題ないことがわかります。

ただし、出力が終わるまで入力を読み込み続けてしまうので、$afalseが入り込んでしまいます。そのため foreach の中で$r &&が必要になっています。

あとは、出力文字列$sの初期化を false で行っているあたりも工夫ポイントです。