🦔

PHPでFizzBuzzをできるだけ短く解いてみた

2024/03/10に公開3

結論

PHPerKaigi2024のコードゴルフ企画でFizzBuzzが出題されたので解いてみました。PHPerKaigi2024の採点システム上ではこのコードが一番短いと思います。これで60bytesだと思います。

for(;@$i++<100;)echo@["Fizz"][$i%3].@["Buzz"][$i%5]?:$i,"
"; //60bytes

本題

こんにちは、notch_manです。先日参加したPHPerKaigi2024でコードゴルフ企画があり、FizzBuzzが出題されたので解いてみました。

提出コードは冒頭の物で何と1位になりました。ただ、手元では57bytesのコードも実装していたのでそれも供養します。

for(;$i++<100;)echo["Fizz"][$i%3].["Buzz"][$i%5]?:$i,"
"; //57bytes

このコードはnoticeやwarningが出てしまい採点システムに弾かれてしまいます。

解説

解説は冒頭のコードで説明していきます。まず、for文でいきなり未定義変数を使っていますが、これは以下の仕様を利用しています。

https://www.php.net/manual/ja/language.types.integer.php#language.types.integer.casting-from-null

この仕様はnullを数値として扱うとゼロになるというものです。これを利用してnullの変数に対してインクリメントすることで暗黙の型変換が行なわれ1,2,3,....となるのです。

次にecho@["Fizz"][$i%3].@["Buzz"][$i%5]の部分です。ここでFizz/Buzz/FizzBuzzの文字列を生成します。FizzBuzzは3で割り切ればFizz、5で割り切ればBuzz、15で割り切ればFizzBuzzを出力します。15で割り切れるは「3で割り切れるかつ5で割り切れる」と等しいのでわざわざ条件式を追加する必要はありません。

ここで普通の発想であれば以下のコードを実装するのではないでしょうか?

($i%3?"":"Fizz").($i%5?"":"Buzz")

実はこの方法で65bytesの回答を得られるのです。これでもかなり短い!!

ここでFizzを出す条件がi%3=0になるということに注意すれば、配列の未定義領域の参照はnullになる性質を利用して要素1の配列を使ってFizz or nullを出すという発想を得られます。Buzzの場合も同様です。これで三項演算子より短く同等の表現を得ることができます。

自分も三項演算子を使いたくないので頑張って色々考えてようやく気づけたのでこれをすぐに気づくのは難しいと思います(笑)ちょうどuzullaさんやshunsockさんと話しながら配列とか使えないのかな~みたいな雑談をしていて着想を得ました。

https://www.php.net/manual/ja/language.types.array.php

これにより配列アクセスにi%3==0 and i%5!=0の時はFizz.nullFizzを得られ、i%3!=0 and i%5==0の時はnull.BuzzFizzを得られ、i%3==0 and i%5==0のときはFizz.BuzzFizzBuzzを得られることができます。ちなみに文字列演算においてはnullが空文字列として扱われる性質を利用しています。

両者がnullの時はnull.nullが空文字列になり、エルビス演算子で数値を出すことができます。これで題意を満たせるコードになります。

最後の改行ですが普通であれば\nを書くと思います。しかし、改行を入力すれば内部ではLFコード0Aが1bytesで格納されるので1トークンで表現することができます。つまり、1bytes削れるわけですね。

学びになったこと

null.nullは文字列になるみたいです。初めて知りましたがPHPは暗黙の型変換がたくさんありますね...。困ったときはgettypeを使ったりPHPマニュアルを見に行きましょう!ちなみにPHPマニュアルには「nullは常に文字列に変換されます」と書かれていました。
https://www.php.net/manual/ja/language.types.string.php

@はエラー制御演算子というもので、式により生成されたエラーメッセージを無視するというものです。初めて知ったし、余程のことが無い限り業務でも使わないですね(笑)
https://www.php.net/manual/ja/language.operators.errorcontrol.php

オチ

ChatGPTにも回答させてみました。

<?php
for($i=1;$i<=100;$i++)echo($i%3?($i%5?$i:'Buzz'):($i%5?'Fizz':'FizzBuzz'))."\n";

これは80bytesだと思います。なお、色々プロンプトを入れて頑張ってみましたがどう頑張っても70bytes台が限界みたいです。60bytes台に乗ればChatGPTに勝利したと言っても良いでしょう(笑)

なお、エラー制御演算子を使わない回答は65bytesになります。エラーを潰すのはアウトローな感じがしますね(笑)

for($i=1;$i++<100;)echo($i%3?"":"Fizz").($i%5?"":"Buzz")?:$i,"
"; //65bytes

Discussion

rana_kualurana_kualu

最後を,~ ;にすれば1バイト減らせると思うのですが、ここのシステムでは駄目だったのでしょうか?
誰もやってる人がいなかったのがちょっと気になったので。

※空白はchr(245)です

nsfisisnsfisis

存在しない定数を参照したときにその定数の名前の文字列として評価される仕様は、このシステムで使われている PHP 8.2 では廃止されているため、 chr(245) の生バイトをただ配置しても、未定義定数のエラーになります。~ を使う場合、クォートで囲って ~"<245>" の 4バイトになってしまうので "<10>" の 3バイトよりも長くなってしまいます。

rana_kualurana_kualu

まじだ!
今試したらエラー吐いた!

前試したときはできていた気がするのでコメント書いたのですが、どうも勘違いしていたようです。失礼しました。