【JS体操】第3問「Zalgo Text の生成」〜いろんな回答紹介編〜

こんにちは!面白プロデュース事業部のおばらです。

『JS体操』第3問 「Zalgo Text の生成」、今回もたくさんの方が挑戦してくださいました。ありがとうございます!本記事はその解説記事の第1弾。挑戦してくださったみなさまの回答、JS体操 QA チームが事前に検証・想定していた回答を一挙にご紹介します。

もし第3問まだ挑戦できていなかった!というひとは以下よりぜひ。

JS体操第3問「Zalgo Text の生成」
JS体操第3問「Zalgo Text の生成」編集画面 JS体操第3問「Zalgo Text の生成」編集画面(エラー)

hubspot.kayac.com


今回の第3問では事前にヒント記事を4件書いてみました。ぜひ併せてご覧ください。

techblog.kayac.com

techblog.kayac.com

techblog.kayac.com

techblog.kayac.com


目次


本戦上位3名の回答

本戦(延長戦前)の上位3名は以下のみなさまでした。1位の halwhite さんは3連覇ですね!CONGRATULATIONS♪


🥇 114文字 by halwhite さん

export default(s,r=Math.random,z=n=>n>0?String.fromCharCode(768+r()*112)+z(n-1):'')=>s.replace(/./g,c=>c+z(r()*8))

再帰を使ったとてもシンプル&スマートな回答。
この方法は運営側では想定できていませんでした。こんなアプローチがあったとは。感激です。
ちなみに再帰を使う回答はこの1件のみ。UNBELIEVABLE♪


🥈 117文字 by ほーく さん

export default(s,r=Math.random)=>s.replace(/./g,c=>{for(s=r()*8;0<s--;)c+=String.fromCharCode(r()*112+768);return c})

素直に for 文を使ったほうが短くなるケースもあるのですね!ループを逆から回していたり、不要になった引数 s を再利用している点もさすがです。AMAZING♪


🥉 119文字 by すぎゃーん さん

export default(s,r=_=>0|Math.random()*112+768)=>s.replace(/./g,c=>c+String.fromCharCode(...[...Array(r()%8+1)].map(r)))

「1文字以上8文字以下の結合文字を付与する」という要件と「1128 の倍数である」ことをうまく利用していますね!Array コンストラクタとスプレッド構文を組み合わせ、短い文字数で密な配列を生成している点も AMAZING♪



社内で元々想定していた最短文字数の回答

114文字 by 社内 QA チーム

export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=_=>768|Math.random()*112,...Array(s()%8)].map(s)))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=_=>768+Math.random()*112,...Array(s()&7)].map(s)))

出題時点で想定していた最短文字数は halwhite さんの回答と同じく114文字でした。ロジックは3位のすぎゃーんさんの回答とほぼ同じ。+1 の回避方法がポイントです。
(ところがその後114文字からさらに短くできたとは、、、)



延長戦の回答

まず、halwhite さんの再帰を使うアプローチが想定外でした。そして再帰を使うと、想定最短文字数の114文字より更に短くできることが判明。そこで急遽延長戦を行うことにしました。詳細は以下の記事をご覧ください。

JS体操第3問「Zalgo Text の生成」

techblog.kayac.com


🥇 111文字 by halwhite さん

export default(s,r=Math.random)=>s.replace(/./g,c=>(s=n=>n>0?s(n-1)+String.fromCharCode(768+r()*112):c)(r()*8))

延長戦開始直後、みごと自己記録更新!元の114文字のコードと見比べるとその発想の転換が素晴らしい。これは私も思いつきませんでした。当初社内では112文字が限界かなと思っていたのでみんなでびっくりしました。


halwhite さんの本戦1位の114文字を社内 QA チーム総動員で短くしようとした回答

再帰を使うアプローチは想定外だったので、もしや114文字からさらに短く出来るのでは?と社内 QA チーム総動員で検証しました。その回答をいくつかご紹介します。


114文字

export default(s,r=Math.random,z=n=>n>0?String.fromCharCode(768+r()*112)+z(n-1/8):'')=>s.replace(/./g,c=>c+z(r()))

r()*8r() にし、n-1n-1/8 にしてみましたが文字数は変わりませんでした。要件が「8文字以下」ではなく「10文字以下」だったら n-1/8n-.1 にできて1文字減らせましたね!


113文字 by 社内 QA チーム

export default s=>s.replace(/./g,c=>c+(s=Math.random,c=n=>n>0?String.fromCharCode(768+s()*112)+c(n-1):'')(s()*8))

不要な変数(引数)sc を再利用して1文字減らしました。



halwhite さんの延長戦111文字を社内 QA チーム総動員で短くした回答

延長戦では112文字が限界かな〜と思っていたら、halwhite さんがなんと111文字を達成。そのロジックも想定外だったのでまた慌てて社内 QA チーム総動員で検証しました。

110文字 by 社内 QA チーム

export default(s,r=Math.random)=>s.replace(/./g,c=>(s=n=>~--n?s(n)+String.fromCharCode(768+r()*112):c)(r()*8))

n>0n-1~--n にまとめて1文字減らしました。


103文字 by 社内 QA チーム

本来はテストで封じたかった若干結果が怪しい回答ですが参考までに。

export default s=>s.replace(/./g,c=>(s=n=>n>0?s(n-1)+String.fromCharCode(768+n*14):c)(Math.random()*8))

Math.random() * 112Math.random() * 8 が含まれている、つまり 1128 の倍数であることを利用しています。 テストには通りますが見た目でも若干規則性が感じられますね。
(もし出題時にこのアプローチに気づいていたらテストで封じたかった。。)


おまけ:String.fromCharCode() / String.fromCodePoint() を使わない回答

String.fromCharCode()String.fromCodePoint() は文字数が多いので、これを使わずに済めば、、とも考えられます。なお Unicode のコードポイントを文字列に変換する方法は以下の記事で検証していますのでぜひご覧ください。 techblog.kayac.com


135文字 eval() + Unicode エスケープシーケンス by 社内 QA チーム

激重注意!

export default s=>s.replace(/./g,c=>c+eval(`''`+`+eval('"\\\\'+'u{'+(768+s()*112|0).toString(16)+'}"')`.repeat(1+(s=Math.random)()*8)))

eval() を2重に使うので激重です。そして文字数はむしろ長くなってしまいました。テストに本当にものすごく時間がかかるのでお試しいただく際は要注意です。


129文字 unescape() を使う回答 by 社内 QA チーム

export default s=>unescape(s.replace(s=/./g,c=>c+c.padEnd((c=Math.random)()*8+1).replace(s,_=>'%u0'+(768|c()*112).toString(16))))

懐かしの unescape() と「%エンコード」で。素直に String.fromCharCode() を使うほうが短くできそうです。


おまけ:eval() + String.fromCharCode() シリーズ

115文字 eval() を使う回答 by 社内 QA チーム

export default s=>s.replace(/./g,c=>c+eval(`String.fromCharCode(${"768+s()*112,".repeat(1+(s=Math.random)()*8)})`))

こんな回答も出題時に想定していました。こちらもテストにものすごく時間がかかるのでお試しいただく際は要注意です。


113文字 eval() を使う回答 by 社内 QA チーム

export default(s,r=Math.random)=>s.replace(/./g,c=>eval('c'+'+String.fromCharCode(768+r()*112)'.repeat(1+r()*8)))

eval() での115文字の回答を、延長戦の期間に113文字にできました。こちらもテストにものすごく時間がかかります。



おまけ:NG 集

Math.random() 以外の方法でランダムっぽい数値を生成するのも面白いテーマですね。今回はテストを厳しめにして封じてしまいましたが、供養のためにいくつかご紹介します。


121文字 Math.random() を使わずに頑張ったがテストにぎりぎり通らない回答①

export default(g=>s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=g++,...Array(g%8)].map(_=>768+((s^=s*4)>>>0)%112))))``


119文字 Math.random() を使わずに頑張ったがテストにぎりぎり通らない回答②

export default(n=>s=>s.replace(/./g,c=>c+String.fromCharCode(...[...Array(-~(n=n*9%8))].map(_=>768+(n=n*9%8)*14))))(.7)


114文字 Math.random() を使わずに頑張ったがテストにぎりぎり通らない回答③

export default((i,j=i)=>s=>s.replace(/./g,c=>c+String.fromCharCode(...[...Array(1+i++%8)].map(_=>768+j++%112))))``


いろんな回答一覧

最後に上で紹介しきれなかったものも含め、上位3名の方の回答とそれを元に QA チームで検証した回答、そして元々想定していた回答を文字数順にたくさん載せておきます。
eval() を使うものなど、処理が重い回答もあるので注意してください)

export default(s,r=Math.random,z=n=>~n?String.fromCharCode(768+r()*112)+z(n-1):String.fromCharCode(768+r()*112)+'')=>s.replace(/./g,c=>c+z(r()*8-2))
export default(s,r=_=>768|Math.random()*112,z=n=>~n?String.fromCharCode(r())+z(n-1):String.fromCharCode(r())+'')=>s.replace(/./g,c=>c+z(r()%8-1))
export default s=>s.replace(/./g,c=>c+eval(`"${Array(1+(s=Math.random)()*8|0).fill().map(_=>`\\u{${(768+s()*112|0).toString(16)}}`).join``}"`))
export default s=>s.replace(/./g,c=>c+eval(`"${c.padEnd(1+8*(c=16,s=Math.random)()).replace(/./g,_=>`\\u0${(s()*c*7|c*c*3).toString(c)}`)}"`))
export default s=>s.replace(/./g,c=>c+eval(`"${Array(1+(s=Math.random)()*8|0).fill().map(_=>`\\u0${(768+s()*112|0).toString(16)}`).join``}"`))
export default s=>s.replace(/./g,c=>c+eval(`"${Array(1+(s=Math.random)()*8|0).fill().map(_=>`\\u0${(s()*112|768).toString(16)}`).join``}"`))
export default s=>s.replace(/./g,c=>c+eval(`"${[s=Math.random,...Array(s()*8|0)].map(_=>`\\u0${(s()*112|768).toString(16)}`).join``}"`))
export default s=>s.replace(s=/./g,c=>c+eval(`"${c.padEnd(1+8*(c=Math.random)()).replace(s,_=>`\\u0${(c()*112|768).toString(16)}`)}"`))
export default s=>s.replace(/./g,c=>c+eval(`''`+`+eval('"\\\\'+'u{'+(768+s()*112|0).toString(16)+'}"')`.repeat(1+(s=Math.random)()*8)))
export default s=>unescape(s.replace(s=/./g,c=>c+c.padEnd((c=Math.random)()*8+1).replace(s,_=>'%u0'+(768|c()*112).toString(16))))
export default s=>s.replace(s=/./g,c=>c+(Array((c=_=>768|Math.random()*112)()%8+2)+'').replace(s,_=>String.fromCharCode(c())))
export default s=>s.replace(s=/./g,c=>c+(1e7+'').slice(8*(c=Math.random)()).replace(s,_=>String.fromCharCode(768|112*c())))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=_=>768+Math.random()*112,...Array((s()-768)/14|0)].map(s)))
export default(s,r=Math.random,z=n=>String.fromCharCode(768+r()*112)+(n>0?z(n-.125):''))=>s.replace(/./g,c=>c+z(r()-.125))
export default(s,r=_=>768|Math.random()*112,z=n=>String.fromCharCode(r())+(~n?z(n-1):''))=>s.replace(/./g,c=>c+z(r()%8-1))
export default(s,r=_=>768|Math.random()*112,z=n=>n>0?String.fromCharCode(r())+z(n-1):'')=>s.replace(/./g,c=>c+z(r()%8+1))
export default(s,r=_=>768|Math.random()*112,z=n=>String.fromCharCode(r())+(n>0?z(n-1):''))=>s.replace(/./g,c=>c+z(r()%8))
export default s=>s.replace(/./g,c=>c+String.fromCharCode((s=_=>768|112*Math.random())(),...Array(s()%8).fill().map(s)))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...Array(-~(8*(s=Math.random)())).fill().map(_=>768|112*s())))
export default s=>s.replace(s=/./g,c=>c+`${c=Math.random,10**(8*c()|0)}`.replace(s,_=>String.fromCharCode(768|112*c())))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=_=>Math.random()*8,...Array(s()|0)].map(_=>768+s()*14)))
export default(s,r=_=>768|Math.random()*112,z=n=>n>-1?String.fromCharCode(r())+z(n-1):'')=>s.replace(/./g,c=>c+z(r()&7))
export default(s,i,r=Math.random)=>s.replace(/./g,c=>{for(i=r()*8;0<i--;)c+=String.fromCharCode(r()*112+768);return c})
export default(s,r=_=>768|Math.random()*112,z=n=>String.fromCharCode(r())+(n?z(n-1):''))=>s.replace(/./g,c=>c+z(r()%8))
export default s=>s.replace(s=/./g,c=>c+c.padEnd(1+8*(c=Math.random)()).replace(s,_=>String.fromCharCode(768|112*c())))
export default s=>s.replace(s=/./g,c=>c+c.repeat((c=Math.random)()*8+1).replace(s,_=>String.fromCharCode(768|112*c())))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[...c.padEnd(1+8*(s=Math.random)())].map(_=>768|112*s())))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[...Array(1+(s=Math.random)()*8|0)].map(_=>768+s()*112)))
export default(s,r=_=>768|Math.random()*112)=>s.replace(/./g,c=>c+(c=n=>~n?String.fromCharCode(r())+c(n-1):'')(r()&7))
export default(s,z)=>s.replace(/./g,c=>c+(z=n=>n++?String.fromCharCode(768+s()*112)+z(n):'')(~(s=Math.random,s()*8)))
export default(s,r=Math.random,z=_=>s--?String.fromCharCode(768+r()*112)+z():'')=>s.replace(/./g,c=>c+z(s=r()*8+1|0))
export default(s,r=Math.random,z=_=>String.fromCharCode(768+r()*112)+(s--?z():''))=>s.replace(/./g,c=>c+z(s=r()*8|0))
export default(s,r=Math.random,z=n=>n>-1?String.fromCharCode(768+r()*112)+z(n-1):'')=>s.replace(/./g,c=>c+z(r()*8-1))
export default(s,r=Math.random,f=(n=r()*8)=>n>0?String.fromCharCode(768|r()*112)+f(n-1):'')=>s.replace(/./g,c=>c+f())
export default(s,f=(n=(s=Math.random)()*8)=>n>0?String.fromCharCode(768|s()*112)+f(n-1):'')=>s.replace(/./g,c=>c+f())
export default s=>s.replace(/./g,c=>c+(s=_=>768|Math.random()*112,c=n=>~n?String.fromCharCode(s())+c(n-1):'')(s()&7))
export default(s,r=Math.random)=>s.replace(/./g,c=>{for(s=r()*8;0<s--;)c+=String.fromCharCode(r()*112+768);return c})
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=Math.random,...Array(s()*8|0)].map(_=>768+s()*112)))
export default s=>s.replace(/./g,c=>c+(c=(n=(s=Math.random)()*8)=>n>0?String.fromCharCode(768|s()*112)+c(n-1):'')())
export default(s,r=Math.random,z=n=>n--?String.fromCharCode(768+r()*112)+z(n):'')=>s.replace(/./g,c=>c+z(1+r()*8|0))
export default(s,r=Math.random,z=n=>String.fromCharCode(768+r()*112)+(n--?z(n):''))=>s.replace(/./g,c=>c+z(r()*8|0))
export default s=>s.replace(/./g,c=>c+eval(`String.fromCharCode(${"768+s()*112,".repeat(1+(s=Math.random)()*8)})`))
export default(s,z=n=>n++?String.fromCharCode(768+s()*112)+z(n):'')=>s.replace(/./g,c=>c+z(~(s=Math.random,s()*8)))
export default(s,z=n=>n++?String.fromCharCode(768+s()*112)+z(n):'')=>s.replace(/./g,c=>c+z(~((s=Math.random)()*8)))
export default(s,r=Math.random,z=n=>n++?String.fromCharCode(768+r()*112)+z(n):'')=>s.replace(/./g,c=>c+z(~(r()*8)))
export default(s,r=Math.random,z=n=>n>0?String.fromCharCode(768+r()*112)+z(n-.125):'')=>s.replace(/./g,c=>c+z(r()))
export default(s,r=Math.random,z=n=>n>0?String.fromCharCode(768+r()*112)+z(n-1/8):'')=>s.replace(/./g,c=>c+z(r()))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=_=>768|Math.random()*112,...Array(s()%8)].map(s)))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=_=>768+Math.random()*112,...Array(s()&7)].map(s)))
export default(s,r=Math.random,z=n=>n>0?String.fromCharCode(768+r()*112)+z(n-1):'')=>s.replace(/./g,c=>c+z(r()*8))
export default(s,z=n=>n>0?String.fromCharCode(768+s()*112)+z(n-1):'')=>s.replace(/./g,c=>c+z((s=Math.random)()*8))
export default s=>s.replace(/./g,c=>c+(c=n=>n++?String.fromCharCode(768+s()*112)+c(n):'')(~(s=Math.random,s()*8)))
export default s=>s.replace(/./g,c=>c+(s=Math.random,c=n=>n++?String.fromCharCode(768+s()*112)+c(n):'')(~(s()*8)))
export default s=>s.replace(/./g,c=>c+(s=Math.random,c=n=>n>0?String.fromCharCode(768+s()*112)+c(n-1):'')(s()*8))
export default(s,r=Math.random)=>s.replace(/./g,c=>eval('c'+'+String.fromCharCode(768+r()*112)'.repeat(1+r()*8)))
export default s=>s.replace(/./g,c=>c+(s=Math.random,c=n=>~--n?String.fromCharCode(768+s()*112)+c(n):'')(s()*8))
export default(s,r=Math.random)=>s.replace(/./g,c=>(s=n=>n>0?s(n-.125)+String.fromCharCode(768+r()*112):c)(r()))
export default(s,r=Math.random)=>s.replace(/./g,c=>(s=n=>n>0?s(n-1)+String.fromCharCode(768+r()*112):c)(r()*8))
export default(s,r=Math.random)=>s.replace(/./g,c=>(s=n=>~--n?s(n)+String.fromCharCode(768+r()*112):c)(r()*8))
export default s=>s.replace(/./g,c=>c+(c=n=>n>0?String.fromCharCode(768+14*n)+c(n-1):'')(Math.random()*8))
export default s=>s.replace(/./g,c=>(s=n=>n>0?s(n-1)+String.fromCharCode(768+n*14):c)(Math.random()*8))



まとめ

最後まで読んでいただきありがとうございます。
そして挑戦してくださったみなさま、ありがとうございました。

今回の第3問、想定していた最短文字数がどんどん更新されていくのにとても驚きましたし楽しかったです。再帰のアプローチを見たときにはうぉぉ、と感動しました。 1位の halwhite さんは第1問の44文字、第2問(正攻法)の66文字、今回第3問本戦の114文字、延長戦の111文字、全てで1位です!すごすぎます。

さて、ご紹介した様々な回答、いかがでしたか? このアプローチは思いついてた!とか、これは思いつきそうだけど思いつけなかった!とか、こうすればあと1文字減らせたのか!など楽しめていただけていれば嬉しいです。 答えを見た後だと簡単に思いつきそうだけど、なぜか思いつかない。不思議です。それがまたコードゴルフの楽しさでもありますね。

『JS体操』はその名の通り、きつい筋トレでもなくピリピリした競技でもなく、ゆるーい頭の体操。 だからこそいろんなアプローチで解けるような楽しい問題をこれからも出題していく予定です。 第4問、第5問といまアイディアを練っていますので、お楽しみに!

次回の記事は第3問の解説第2弾。
いくつかの回答の詳しい解説と、今回の問題の応用例などを紹介します。
ぜひご期待ください。



『JS体操』の情報を受け取ろう

登録フォーム

次回以降の「解説ブログ」や「JS体操の問題」のお知らせが気になる方は以下のページにてご登録ください。

hubspot.kayac.com

技術部公式 X アカウント

面白法人カヤック技術部公式 X アカウント @kayac_tech でも随時情報を発信します。

カヤック技術部公式 X アカウント @kayac_tech


『JS体操』過去問一覧

『JS体操』の過去問、まだ挑戦していない!という方はぜひ。

hubspot.kayac.com hubspot.kayac.com hubspot.kayac.com


お知らせ

先日、Perl のコードゴルフコンテストもカヤック主催で開催されました。
こちらも面白いのでぜひご覧ください!

techblog.kayac.com

そして、カヤックではコードゴルフが大好きな新卒&中途エンジニアも募集しています!

www.kayac.com www.kayac.com