たまたまJavascriptを1行で書く - Qiitaという記事を見つけて、そういえばcodegolfなんてものがあったなと思い出して挑戦してみました。
【ファン迷惑】「響け!ユーフォニアム」という文字列だけで遊ぶシェル芸人達 - Togetter
この記事ではFoo.protoype.barはFoo::barと表記します (例: Array.prototype.join -> Array::join)
とりあえず大本のコード何個か考える
A
var a = "響け!ユーフォニアム".split("");
for(var i = 0; i < a.length; i++){
console.log(a.join(""));
a.unshift(a.pop());
}
Togetterのほうにあるコードですね。
a.unshift(a.pop())がなるほどって感じでした。
B
var b = "響け!ユーフォニアム";
b.forEach( (_, i) => {
console.log(b.slice(i) + b.slice(0, i));
});
Togetterのほうにあるコードの二つ目です。
C
var b = "響け!ユーフォニアム";
[b].concat(b.split("")).reduce( (s, e) => {
console.log(s);
return s.slice(1)+e;
});
なんとなくArray::reduce使いたいなって思って書きました。
D
var a = "響け!ユーフォニアム";
for (var i in a) {
console.log(a);
a = a.replace(/(.)(.*)/,"$2$1");
}
正規表現でいくパターンです。
正直短くならないと思います。
一番直感的な気はしますが。
読める程度に短くする
読める程度にvar1や改行や不要なコロンを取り除いていきます。
また、String::splitを[...]で書き直していきます。
同時にJavascriptでは複数のループ方法があるので、それぞれに書き直してみます。
Array::forEachはArray::mapで返り値を捨てれば同じことになるのでそっちで統一します。
/* ----- A ----- */
a=[..."響け!ユーフォニアム"];
// for
for(i = 0; i < a.length; i++){console.log(a.join(""));a.unshift(a.pop())}
// for in
for(i in a){console.log(a.join(""));a.unshift(a.pop())}
// map
a.map( _ => {console.log(a.join(""));a.unshift(a.pop())})
/* ----- B ----- */
b="響け!ユーフォニアム";
// for
for(i = 0; i < b.length; i++){console.log(b.slice(i) + b.slice(0, i))}
// for in
for(i in b){console.log(b.slice(i) + b.slice(0, i))}
// map
b.map( (_, i) => {console.log(b.slice(i) + b.slice(0, i))})
/* ----- C ----- */
b="響け!ユーフォニアム";
[b, ...b].reduce( (s, e) => {console.log(s);return s.slice(1)+e})
/* ----- D ----- */
b="響け!ユーフォニアム";
// for
for(i = 0; i < b.length; i++){console.log(b);b=b.replace(/(.)(.*)/,"$2$1")}
// for in
for (i in b) {console.log(b);b=b.replace(/(.)(.*)/,"$2$1")}
なんとなくcodegolf感が出てきました
関数を工夫する
同じようなことをするにも別の関数を使ったり、別の方法で使ったりすることで短くなることもあります。
少し書き換えてみます。
for ofが先ほどないなって思った方もいらっしゃると思いますが、ここで書き直します。
A
a=[..."響け!ユーフォニアム"];
// for
for(i = 0; i < a.length; i++){console.log(a.join(""));a.push(a.shift())}
// for in
for(i in a){console.log(a.join(""));a.push(a.shift())}
// map
a.map( _ => {console.log(a.join(""));a.push(a.shift())})
回転方向は関係ない(はず)なので、a.unshift(a.pop())をa.push(a.shift())に書き換えます。
これで1文字減ります。
D
b="響け!ユーフォニアム";
// for
for(i = 0; i < b.length; i++){console.log(b);b=b.replace(/(.)(.*)/,"$2$1")}
// for in
for (i in b) {console.log(b);b=b.replace(/(.)(.*)/,"$2$1")}
// for of
for(s of b) {console.log(b);b=b.replace(/.(.*)/,"$1"+s)}
// for of 2
for(s of b) {console.log(b);b=b.replace(s,"")+s}
// for of 3
for(s of b) {console.log(b);b=b.slice(1)+s}
// for in 4
for(i in b) {console.log(b);/./.exec(b);b=RegExp["$'"]+RegExp["$&"]}
// for of 4
for(s of b) {console.log(b);/./.exec(b);b=RegExp["$'"]+s}
for ofを使うことでb[i]や$1をsとして利用できるので少し短くなりました。
もしかしたらと思って、RegExp["$'"]などの非推奨のものでも書いてみました。
正規表現が/./の短さになるのはメリットかなと。
b="響け!ユーフォニアム";
for(i in b)console.log(b.replace(eval(`/(.{${i}})(.*)/`),"$2$1"));
というのも書いてみたんですが、長すぎますね…
forやmapの中身の一行化
forの中身を一行にすることで{}を省略できます。
mapなどは内側の即時関数を()=>{return~}から_=>~にできます。
一行の場合は最後が自動でreturnされるのが大きいですね。
そこで一行への仕方ですが、何個か方法があります。
- カンマ演算子(
,)の利用- 関数の引数には使えません
- 例:
func(1, 2)->引数に1と2を渡すことになるからNG、func((1, 2))ならOK
- 論理和/差演算子(
||/&&)の利用- 先に来るものが必ず
trueもしくはfalseに変換できる必要があります - 例:
console.log()||a+b,a-b&&console.log()(a-bは0にはならない場合)
- 先に来るものが必ず
- ビット和/差演算子(
|/&)の利用- 先に来るものが必ず数値に変換でき
0もしくはそれ以外に変換できる必要があります - また、返り値が必要な場合は使えません(数値変換されるので)
- 例:
console.log()|a+b
- 先に来るものが必ず数値に変換でき
カンマ演算子が一番考えることが少ないので(たぶん)、優先したいですね。
/* ----- A ----- */
a=[..."響け!ユーフォニアム"];
// for
for(i = 0; i < a.length; i++)console.log(a.join("")),a.push(a.shift())
// for in
for(i in a)console.log(a.join("")),a.push(a.shift())
// map
a.map( _ => console.log(a.join(""))|a.push(a.shift()))
/* ----- B ----- */
b = "響け!ユーフォニアム";
// for
for(i = 0; i < b.length; i++)console.log(b.slice(i) + b.slice(0, i))
// for in
for(i in b)console.log(b.slice(i) + b.slice(0, i))
// map
b.map( (_, i) => console.log(b.slice(i) + b.slice(0, i)))
/* ----- C ----- */
b = "響け!ユーフォニアム";
[b, ...b].reduce( (s, e) => console.log(s)||s.slice(1)+e)
/* ----- D ----- */
b = "響け!ユーフォニアム";
// for
for(i = 0; i < b.length; i++)console.log(b),b=b.replace(/(.)(.*)/,"$2$1")
// for in
for (i in b)console.log(b),b=b.replace(/(.)(.*)/,"$2$1")
// for of
for(s of b)console.log(b),b=b.replace(/.(.*)/,"$1"+s)
// for of 2
for(s of b)console.log(b),b=b.replace(s,"")+s
// for of 3
for(s of b)console.log(b),b=b.slice(1)+s
// for in 4
for(i in b)console.log(b),/./.exec(b),b=RegExp["$'"]+RegExp["$&"]
// for of 4
for(s of b)console.log(b),/./.exec(b),b=RegExp["$'"]+s
だいぶ短くなってきましたね。
()などの除去
テンプレートレテラルを用いてfunc("文字列")はfunc`文字列`に書き換えます。
空白の完全除去も同時に行います。
/* ----- A ----- */
a=[..."響け!ユーフォニアム"];
// for
for(i=0;i<a.length;i++)console.log(a.join``),a.push(a.shift())
// for in
for(i in a)console.log(a.join``),a.push(a.shift())
// map
a.map(_=>console.log(a.join``)|a.push(a.shift()))
/* ----- B ----- */
b="響け!ユーフォニアム";
// for
for(i=0;i<b.length;i++)console.log(b.slice(i)+b.slice(0,i))
// for in
for(i in b)console.log(b.slice(i)+b.slice(0,i))
/* ----- C ----- */
b="響け!ユーフォニアム";
[b,...b].reduce((s,e)=>console.log(s)||s.slice(1)+e)
/* ----- D ----- */
b="響け!ユーフォニアム";
// for of
for(s of b)console.log(b),b=b.replace(/.(.*)/,"$1"+s)
// for of 2
for(s of b)console.log(b),b=b.replace(s,"")+s
// for of 3
for(s of b)console.log(b),b=b.slice(1)+s
// for of 4
for(s of b)console.log(b),/./.exec(b),b=RegExp["$'"]+s
いくつか明らかに無理そうなのは削除しました。(for inでiを使っていなかったりfor ofでsを使っていなかったり)
変数代入やインクリメントの移動
例としてAのforのバージョンを使います。
b="響け!ユーフォニアム";
for(i=0;i<b.length;i++)console.log(b.slice(i)+b.slice(0,i))
iを上へa.lengthに向かってインクリメントしてますが、a.lengthから始めてデクリメントするようにします。
b="響け!ユーフォニアム";
for(i=b.length;i;i--)console.log(b.slice(i)+b.slice(0,i))
こうすると、終了条件がiになって2文字減ります。2
b="響け!ユーフォニアム";
for(i=b.length;i;)console.log(b.slice(i)+b.slice(0,i--))
インクリメントもfor文内に突っ込むとさらに1文字減ります。
ほかの文にも同じようにしていきます。
/* ----- A ----- */
a=[..."響け!ユーフォニアム"];
// for
for(i=a.length;i;i--)console.log(a.join``),a.push(a.shift())
// for in
for(i in a)console.log(a.join``),a.push(a.shift())
// map
a.map(_=>console.log(a.join``)|a.push(a.shift()))
/* ----- B ----- */
b="響け!ユーフォニアム";
// for
for(i=b.length;i;)console.log(b.slice(i)+b.slice(0,i--))
// for in
for(i in b)console.log(b.slice(i)+b.slice(0,i))
/* ----- C ----- */
[b="響け!ユーフォニアム",...b].reduce((s,e)=>console.log(s)||s.slice(1)+e)
/* ----- D ----- */
b="ム響け!ユーフォニア";
// for
for(i=b.length;i;i--)console.log(b=b.replace(/(.)(.*)/,"$2$1"))
// for of
for(s of b)console.log(b=b.replace(/.(.*)/,"$1"+s))
// for of 2
for(s of b)console.log(b=b.replace(s,"")+s)
// for of 3
for(s of b)console.log(b=b.slice(1)+s)
// for of 4
for(s of b)/./.exec(b),console.log(b=RegExp["$'"]+s)
"ム響け!ユーフォニア"というのはせこい気もしますが…
結果
/* ----- A ----- */
a=[..."響け!ユーフォニアム"];a.map(_=>console.log(a.join``)|a.push(a.shift()))
/* ----- B ----- */
for(i in b="響け!ユーフォニアム")console.log(b.slice(i)+b.slice(0,i))
/* ----- C ----- */
[b="響け!ユーフォニアム",...b].reduce((s,e)=>console.log(s)||s.slice(1)+e)
/* ----- D ----- */
for(s of b="ム響け!ユーフォニア")console.log(b=b.slice(1)+s)
ということで自分の最短は
for(s of b="ム響け!ユーフォニア")console.log(b=b.slice(1)+s)
の51文字でした
ちなみにCoffeeScriptだと
console.log b=b[1..]+s for s in b="ム響け!ユーフォニア"
の46文字でした
参考
Tips for golfing in JavaScript - StackExchange
Tips for Golfing in ECMAScript 6 and above - StackExchange