整数からRGB空間の色を生成する（その1）

# 動機

2024/04/25開催の[春和の候！若手エンジニアふんわりLT Night！](https://wakate-funwari-study.connpass.com/event/308966)に参加した折、[ひなさん](https://twitter.com/mi111025)のLT「友達にコード送ったら1行にされた」をお聞きしました。

https://speakerdeck.com/mi111025/0425

LTではHSL空間を使ってエレガントに一行にされていたのですが、あえて RGB のままコードをどこまで短縮できるか試してみました。

**この記事のコード辺は効率やメンテナンス性を無視しているので、本番で使ってはいけません。**

:::note info
大まかに色コードを指定できればよさそうだったので、元コードと境界値条件が合っているかは確認していません。間違いがあれば編集リクエストをお願いします。
:::

これが元コードです。（https://speakerdeck.com/mi111025/0425?slide=4 より引用）

[![](https://files.speakerdeck.com/presentations/162505ec6e7445fd8e2fd3b8a9ff9f53/slide_3.jpg)](https://speakerdeck.com/mi111025/0425?slide=4)

まずは今回のゴルフ対象になる関数を次のように決めます。

```js:base code
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

```js:pattern 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()` すれば生成できます。

<details><summary>この時点でのコード</summary>

```js
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()})`;
};
```
</details>

## 整数剰余を使う

元コードの if 分岐部分を書き換えます。

15 にかけられている `range` の範囲を、`[0, 17)` に収めたいようです。
こういう時は割り算のあまりを計算する `%` が使えます。

合わせて、境界値を17単位に変更します（次の項目に効いてきます）。
※ どこの分岐に入るかが変わりますが、出来上がる `result` は変わりません。

<details><summary>この時点でのコード</summary>

```javascript
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()})`;
};
```
</details>

## 条件をまとめる

if 文の条件には `range` の範囲が指定されていますが、`range` を17で割った商の整数部分を見ることで、`&&` なしで同じ判定ができます。
これをやるために、前項で境界値を 17 単位にいじっておいたのです。

[`Math.trunc`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc) は、引数の整数部分を返す関数です。

<details><summary>この時点でのコード</summary>

```javascript
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()})`;
};
```
</details>

## `result` を直接得る

`case` は整数だけなので、配列で置き換えます。かなり完成形に近づいてきました。

この辺りから、製品コードでは使えないテクニックになってきます。

<details><summary>この時点でのコード</summary>

```javascript
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()})`;
};
```
</details>

## 全体を15で割る

プログラム中に 255 がたくさん出てきています。
255 = 17 * 15 なので、`result` 全体を 15 で割ると短縮できそうです。
もちろんそのままだと値が壊れるので、最後に15倍して戻してあげます。

さらに、不要な括弧を取り除いて短縮します。

<details><summary>この時点でのコード</summary>

```javascript
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()})`;
};
```
</details>

## `Math.trunc` を置き換える

先ほど追加した整数化関数ですが、長いのでイディオムに変えます。

整数化には何通りかやり方がありますが、最短は次の2つです。
どちらも JS のビット演算が整数部のみを対象にすることを利用します。

- `x|0`
    - ビットOR演算
- `~~x`
    - ビットNOT演算を2回

もちろん、プロダクションコードで整数化のために使うべきではありません。

演算子の優先順位上、今回は前者を採用します（後者では `~~range / 17` と書くと `(~~range) / 17` になってしまう）。

<details><summary>この時点でのコード</summary>

```javascript
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()})`;
};
```
</details>

## マジックナンバーを置換する

今度は 17 がたくさん出てきているので、変数で持ちます。
1箇所あたり1文字短縮できます。

<details><summary>この時点でのコード</summary>

```javascript
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()})`;
};
```
</details>

スコープを汚染していますが気にしません。

## 文字列を直接返す

短い記法のアロー関数を使えるように、`const result` を削ります。

<details><summary>この時点でのコード</summary>

```javascript
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()})`;
```
</details>

## 変数名を置換

変数名を1文字にします。

<details><summary>この時点でのコード</summary>

```javascript
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()})`;
```
</details>

ここから不要なホワイトスペース文字を削ると、最初のコードを得られます！

# 終わりに

頭の体操としてのコードゴルフは楽しいですね。
まだまだ初心者なので、もっと短くできる方法があれば知りたいです。

この記事を書いている間に案2を思いついたので、改めて投稿します。