🎨

TS/JSでHEX Color形式の文字列をRGB形式のオブジェクトにするワンライナー

2021/07/02に公開

先日、DenoでGitHubの草をターミナルに表示するモジュールを作りました。

https://zenn.dev/kawarimidoll/articles/1679844a116395

こちらでHEX形式の色情報をRGB形式に直していたのですが、ワンライナー化できたので紹介します。

ワンライナー

const hexToRgb = (color = "000") =>
  Object.fromEntries(
    ((color.match(/^#?[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/) ? color : "000").replace(
      /^#?(.*)$/,
      (_, hex) => (hex.length == 3) ? hex.replace(/./g, "$&$&") : hex,
    ).match(/../g) ?? []).map((c, i) => ["rgb".charAt(i), parseInt("0x" + c)]),
  );

途中で改行していますが、すべて改行をなくしても問題ありません。
また、型指定もないので、TypeScriptでもJavaScriptでも動作します [1]

同じ動作をするコード

先日の記事に掲載していたコードはこちらです。

const hexToRgb = (hex: string) => {
  if (hex.slice(0, 1) == "#") hex = hex.slice(1);
  if (hex.length == 3) {
    hex = hex.slice(0, 1) + hex.slice(0, 1) + hex.slice(1, 2) +
      hex.slice(1, 2) + hex.slice(2, 3) + hex.slice(2, 3);
  }

  const r = parseInt("0x" + hex.slice(0, 2));
  const g = parseInt("0x" + hex.slice(2, 4));
  const b = parseInt("0x" + hex.slice(4, 6));

  return { r, g, b };
};

何をしているか

以下の構造になっています。
デフォルト値を黒("000")にしていますが、別に何色でも問題ありません。

const hexToRgb = (color = "000") => {
  const hexStr = 
    color.match(/^#?[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/) ? color : "000";

  const hexCode = hexStr.replace(
    /^#?(.*)$/,
    (_, hex) => (hex.length == 3) ? hex.replace(/./g, "$&$&") : hex,
  );

  const rgbArray = hexCode.match(/../g) ?? [];

  const rgbArrayWithKey = rgbArray.map((
    c,
    i,
  ) => ["rgb".charAt(i), parseInt("0x" + c)]);

  const rgb = Object.fromEntries(rgbArrayWithKey);

  return rgb;
};

バリデーションする

HEX形式として、"#123456"のようにhashと6桁で表示されるもの、"#112233""#123"のように省略されているもの、それぞれの頭のhashが省略されたものを想定しました。
これらに該当しないものは"000"とします。もちろん、デフォルト値を設定するのではなく例外を投げるべき場合もあると思います が、一行で書けることを優先しました

const hexStr = 
  color.match(/^#?[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/) ? color : "000";

形式を調整する

これをString.prototype.replace()で「hashなし6桁」に整形します。
replace()の第2引数の関数で、先頭のhashを除去し、省略形式の場合はそれぞれの文字を繰り返します。

const hexCode = color.replace(
  /^#?(.*)$/,
  (_, hex) => (hex.length == 3) ? hex.replace(/./g, "$&$&") : hex,
);

これで、例えば"#123456"という入力から"123456"という文字列ができます。

2字ずつの配列にする

String.prototype.match()で文字列を2字ずつに分割します。
?? []でnull guardをつけているのはTypeScriptコンパイラに対応するためで、ここでは本質ではないため気にしないでください。

const rgbArray = hexCode.match(/../g) ?? [];

これで、"123456"["12", "34", "56"]という配列になります。

RGBとの対応をつける

Array.prototype.map()String.prototype.charAt()を使って"r" "g" "b"の各文字との対応をつけます。また、parseInt()を使って16進数の文字列を10進数の数値に変換します。

const rgbArrayWithKey = rgbArray.map((
  c,
  i,
) => ["rgb".charAt(i), parseInt("0x" + c)]);

これで、["12", "34", "56"][["r", 18], ["g", 52], ["b", 86]]という配列になります。

別解

ここではrgbArrayの各要素についてループしていますが、逆に"rgb"の各文字についてループするという発想もありますね。

const rgbArrayWithKey = "rgb".split("").map((
      c,
      i,
    ) => [c, parseInt("0x" + rgbArray[i])])

この場合、rgbArrayを介さずhexCode.slice(i * 2, (i + 1) * 2)で文字列を抽出する方法もとれます。

配列をオブジェクト化する

最後に、Object.fromEntries()を使って2次元配列をオブジェクトに変換します。

const rgb = Object.fromEntries(rgbArrayWithKey);

これで、[["r", "18"], ["g", "52"], ["b", "86"]]{r: 18, g: 52, b: 86}というオブジェクトになります。

おわりに

以上をまとめると、最初に出したコードになります。

const hexToRgb = (color = "000") =>
  Object.fromEntries(
    ((color.match(/^#?[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/) ? color : "000").replace(
      /^#?(.*)$/,
      (_, hex) => (hex.length == 3) ? hex.replace(/./g, "$&$&") : hex,
    ).match(/../g) ?? []).map((c, i) => ["rgb".charAt(i), parseInt("0x" + c)]),
  );

先日の記事のコードを変換できたので満足です。
可読性に難があることは否めませんが、変数を除去するのが好きなんですよね…。

わかりづらいコードを書いたときは、それを記事にまとめれば、後から確認できるし知見の共有もできて良いのではないかと思いました。

正規表現楽しい!

参考

https://stackoverflow.com/questions/40358037/doubling-each-letter-in-a-string-in-js
https://qiita.com/turmericN/items/0819317b5c075d971bfa

脚注
  1. TypeScriptの場合、出力に{ r: number; g: number; b: number }の型指定をつける場合があるかもしれません ↩︎

Discussion