動機
2024/04/25開催の春和の候!若手エンジニアふんわりLT Night!に参加した折、ひなさんのLT「友達にコード送ったら1行にされた」をお聞きしました。
LTではHSL空間を使ってエレガントに一行にされていたのですが、あえて RGB のままコードをどこまで短縮できるか試してみました。
この記事のコード辺は効率やメンテナンス性を無視しているので、本番で使ってはいけません。
大まかに色コードを指定できればよさそうだったので、元コードと境界値条件が合っているかは確認していません。間違いがあれば編集リクエストをお願いします。
これが元コードです。(https://speakerdeck.com/mi111025/0425?slide=4 より引用)
まずは今回のゴルフ対象になる関数を次のように決めます。
const getColor = (range) => {
let r, g, b;
if (range >= 0 && range <= 17) {
r = 255;
g = range * 15;
b = 0;
} else if (range >= 18 && range <= 33) {
r = 255 - (range - 17) * 15;
g = 255;
b = 0;
} else if (range >= 34 && range <= 50) {
r = 0;
g = 255;
b = (range - 34) * 15;
} else if (range >= 51 && range <= 67) {
r = 0;
g = 255 - (range - 50) * 15;
b = 255;
} else if (range >= 68 && range <= 83) {
r = (range - 68) * 15;
g = 0;
b = 255;
} else if (range >= 84 && range <= 100) {
r = 255;
g = 0;
b = 255 - (range - 84) * 15;
}
return `rgb(${r}, ${g}, ${b})`;
};
案1
let x=17,c=v=>`rgb(${[[x,v,0],[x-v%x,x,0],[0,x,v%x],[0,x-v%x,x],[v%x,0,x],[x,0,x-v%x]][v/x|0].map(c=>c*15).join()})`
// 116 characters
// c(40) -> 'rgb(0,255,90)'
元コードのロジックを愚直に書き出したパターンです。順にみていきましょう。
RGBの各値を配列で持つ
r, g, b の変数宣言をしないで、[r, g, b]
の配列で持つようにします。
返り値のカンマ区切りの値は、配列を join()
すれば生成できます。
この時点でのコード
const getColor = (range) => {
let result;
if (range >= 0 && range <= 17) {
result = [255, range * 15, 0];
} else if (range >= 18 && range <= 33) {
result = [255 - (range - 17) * 15, 255, 0];
} else if (range >= 34 && range <= 50) {
result = [0, 255, (range - 34) * 15];
} else if (range >= 51 && range <= 67) {
result = [0, 255 - (range - 50) * 15, 255];
} else if (range >= 68 && range <= 83) {
result = [(range - 68) * 15, 0, 255];
} else if (range >= 84 && range <= 100) {
result = [255, 0, 255 - (range - 84) * 15];
}
return `rgb(${result.join()})`;
};
整数剰余を使う
元コードの if 分岐部分を書き換えます。
15 にかけられている range
の範囲を、[0, 17)
に収めたいようです。
こういう時は割り算のあまりを計算する %
が使えます。
合わせて、境界値を17単位に変更します(次の項目に効いてきます)。
※ どこの分岐に入るかが変わりますが、出来上がる result
は変わりません。
この時点でのコード
const getColor = (range) => {
let result;
if (range >= 0 && range < 17) {
result = [255, range * 15, 0];
} else if (range >= 17 && range < 34) {
result = [255 - (range % 17) * 15, 255, 0];
} else if (range >= 34 && range < 51) {
result = [0, 255, (range % 17) * 15];
} else if (range >= 51 && range < 68) {
result = [0, 255 - (range % 17) * 15, 255];
} else if (range >= 68 && range < 84) {
result = [(range % 17) * 15, 0, 255];
} else if (range >= 84 && range <= 100) {
result = [255, 0, 255 - (range % 17) * 15];
}
return `rgb(${result.join()})`;
};
条件をまとめる
if 文の条件には range
の範囲が指定されていますが、range
を17で割った商の整数部分を見ることで、&&
なしで同じ判定ができます。
これをやるために、前項で境界値を 17 単位にいじっておいたのです。
Math.trunc
は、引数の整数部分を返す関数です。
この時点でのコード
const getColor = (range) => {
let result;
switch (Math.trunc(range / 17)) {
case 0:
result = [255, range * 15, 0];
break;
case 1:
result = [255 - (range % 17) * 15, 255, 0];
break;
case 2:
result = [0, 255, (range % 17) * 15];
break;
case 3:
result = [0, 255 - (range % 17) * 15, 255];
break;
case 4:
result = [(range % 17) * 15, 0, 255];
break;
case 5:
result = [255, 0, 255 - (range % 17) * 15];
break;
}
return `rgb(${result.join()})`;
};
result
を直接得る
case
は整数だけなので、配列で置き換えます。かなり完成形に近づいてきました。
この辺りから、製品コードでは使えないテクニックになってきます。
この時点でのコード
const getColor = (range) => {
const result = [
[255, range * 15, 0],
[255 - (range % 17) * 15, 255, 0],
[0, 255, (range % 17) * 15],
[0, 255 - (range % 17) * 15, 255],
[(range % 17) * 15, 0, 255],
[255, 0, 255 - (range % 17) * 15],
][Math.trunc(range / 17)];
return `rgb(${result.join()})`;
};
全体を15で割る
プログラム中に 255 がたくさん出てきています。
255 = 17 * 15 なので、result
全体を 15 で割ると短縮できそうです。
もちろんそのままだと値が壊れるので、最後に15倍して戻してあげます。
さらに、不要な括弧を取り除いて短縮します。
この時点でのコード
const getColor = (range) => {
const result = [
[17, range, 0],
[17 - range % 17, 17, 0],
[0, 17, range % 17],
[0, 17 - range % 17, 17],
[range % 17, 0, 17],
[17, 0, 17 - range % 17],
][Math.trunc(range / 17)];
return `rgb(${result.map(c => c * 15).join()})`;
};
Math.trunc
を置き換える
先ほど追加した整数化関数ですが、長いのでイディオムに変えます。
整数化には何通りかやり方がありますが、最短は次の2つです。
どちらも JS のビット演算が整数部のみを対象にすることを利用します。
-
x|0
- ビットOR演算
-
~~x
- ビットNOT演算を2回
もちろん、プロダクションコードで整数化のために使うべきではありません。
演算子の優先順位上、今回は前者を採用します(後者では ~~range / 17
と書くと (~~range) / 17
になってしまう)。
この時点でのコード
const getColor = (range) => {
const result = [
[17, range, 0],
[17 - range % 17, 17, 0],
[0, 17, range % 17],
[0, 17 - range % 17, 17],
[range % 17, 0, 17],
[17, 0, 17 - range % 17],
][range / 17 | 0];
return `rgb(${result.map(c => c * 15).join()})`;
};
マジックナンバーを置換する
今度は 17 がたくさん出てきているので、変数で持ちます。
1箇所あたり1文字短縮できます。
この時点でのコード
const x=17,getColor = (range) => {
const result = [
[x, range, 0],
[x - range % x, x, 0],
[0, x, range % x],
[0, x - range % x, x],
[range % x, 0, x],
[x, 0, x - range % x],
][range / x | 0];
return `rgb(${result.map(c => c * 15).join()})`;
};
スコープを汚染していますが気にしません。
文字列を直接返す
短い記法のアロー関数を使えるように、const result
を削ります。
この時点でのコード
const x=17,getColor = (range) => `rgb(${[
[x, range, 0],
[x - range % x, x, 0],
[0, x, range % x],
[0, x - range % x, x],
[range % x, 0, x],
[x, 0, x - range % x],
][range / x | 0].map(c => c * 15).join()})`;
変数名を置換
変数名を1文字にします。
この時点でのコード
const x=17,c = v => `rgb(${[
[x, v, 0],
[x - v % x, x, 0],
[0, x, v % x],
[0, x - v % x, x],
[v % x, 0, x],
[x, 0, x - v % x],
][v / x | 0].map(c => c * 15).join()})`;
ここから不要なホワイトスペース文字を削ると、最初のコードを得られます!
終わりに
頭の体操としてのコードゴルフは楽しいですね。
まだまだ初心者なので、もっと短くできる方法があれば知りたいです。
この記事を書いている間に案2を思いついたので、改めて投稿します。