【JS】'rgb[a](R, G, B[, A])' を正規表現で処理して，各値をメンバーとしてもつオブジェクトを返す関数

たとえば，`'rgba(220, 20, 60, 0.5)'` を放り込めば

```javascript
{
	red  : 220,
	green: 20,
	blue : 60,
	alpha: 0.5,
}
```

が返ってくる関数です．

## コード

※ 2020 年 9 月 17 日，[RegExp の複数行記述，RegExp 内での変数参照がしたいので，正規表現を結合する関数を作った - Qiita](https://qiita.com/juthaDDA/items/f1093b968faa3d810c1c) の内容を反映させ更新．

### TypeScript

```typescript:rgba.ts
/**
 * @author JuthaDDA
 * @see [RegExp の複数行記述，RegExp 内での変数参照がしたいので，
 *     正規表現を結合する関数を作った - Qiita
 *     ](https://qiita.com/juthaDDA/items/f1093b968faa3d810c1c)
 * @param regExps - Babel (< 7.11.0) を使う場合は,
 *     名前付きキャプチー・グループを含むと正しく変換されない.
 *     `@babel/plugin-transform-named-capturing-groups-regex (< 7.10.4)` も未対応.
 */
const concatRegExps = ( regExps:RegExp[], flags?:string ):RegExp => {
	return RegExp( regExps.reduce( ( acc, cur ) => acc + cur.source, '' ),
		flags,
	);
};

interface RgbaValues {
	red:number,
	green:number,
	blue:number,
	alpha:number
}

/**
 * @author juthaDDA
 * @param rgba - 'rgb[a](R, G, B[, A])' 形式.
 * @see ['rgb\[a\](R, G, B\[, A\])' を正規表現で処理して，
 *     各値をメンバーとしてもつオブジェクトを返す関数 - Qiita](
 *     https://qiita.com/juthaDDA/items/d81f45295095eb4563f4)
 */
const rgbaStrToValues = ( rgba:string ):RgbaValues|null => {
	/**
	 * ` /[+-]?\d*\.?\d+/` は実数（整数部の 0 省略可）の正規表現.
	 *
	 * @see [数値とマッチする正規表現 - Qiita](
	 *     https://qiita.com/BlueSilverCat/items/f35f9b03169d0f70818b)
	 */
	const regExp = concatRegExps( [
		/^rgba?\( *([+-]?\d*\.?\d+) *, *([+-]?\d*\.?\d+) *, */, // rgb[a](R, G,
		/([+-]?\d*\.?\d+)(?: *, *([+-]?\d*\.?\d+) *)?\)$/, // B[, A])
	] );

	const result = regExp.exec( rgba );
	if ( ! result ) { return null; }

	const { 1: red, 2: green, 3: blue, 4: alpha } = result;
	if ( ! ( red && green && blue ) ) { return null; }

	const { min, max } = Math;
	return {
		red  : max( min( Number( red ), 255 ), 0 ),
		green: max( min( Number( green ), 255 ), 0 ),
		blue : max( min( Number( blue ), 255 ), 0 ),
		alpha: alpha ? max( min( Number( alpha ), 1 ), 0 ) : 1,
	};
};

```
[^RgbaValues]

### JavaScript

<details>
<div>

```javascript:rgba.js
/**
 * @author JuthaDDA
 * @see [RegExp の複数行記述，RegExp 内での変数参照がしたいので，
 *     正規表現を結合する関数を作った - Qiita
 *     ](https://qiita.com/juthaDDA/items/f1093b968faa3d810c1c)
 * @param {RegExp[]} regExps - Babel (< 7.11.0) を使う場合は,
 *     名前付きキャプチー・グループを含むと正しく変換されない.
 *     `@babel/plugin-transform-named-capturing-groups-regex` (< 7.10.4) も未対応.
 * @param {string}   [flags]
 * @return {RegExp}
 */
const concatRegExps = ( regExps, flags ) => {
	return RegExp(
		regExps.reduce( ( acc, cur ) => acc + cur.source, '' ),
		flags,
	);
};

/**
 * @author juthaDDA
 * @see ['rgb\[a\](R, G, B\[, A\])' を正規表現で処理して，
 *     各値をメンバーとしてもつオブジェクトを返す関数 - Qiita](
 *     https://qiita.com/juthaDDA/items/d81f45295095eb4563f4)
 * @param {string} rgba - 'rgb[a](R, G, B[, A])' 形式.
 * @return {{red:number,green:number,blue:number,alpha:number}}
 */
const rgbaStrToValues = ( rgba ) => {
	/**
	 * ` /[+-]?\d*\.?\d+/` は実数（整数部の 0 省略可）の正規表現.
	 *
	 * @see [数値とマッチする正規表現 - Qiita](
	 *     https://qiita.com/BlueSilverCat/items/f35f9b03169d0f70818b)
	 */
	const regExp = concatRegExps( [
		/^rgba?\( *([+-]?\d*\.?\d+) *, *([+-]?\d*\.?\d+) *, */, // rgb[a](R, G,
		/([+-]?\d*\.?\d+)(?: *, *([+-]?\d*\.?\d+) *)?\)$/, // B[, A])
	] );

	const result = regExp.exec( rgba );
	if ( ! result ) { return null; }

	const { 1: red, 2: green, 3: blue, 4: alpha } = result;
	if ( ! ( red && green && blue ) ) { return null; }

	const { min, max } = Math;
	return {
		red  : max( min( Number( red ), 255 ), 0 ),
		green: max( min( Number( green ), 255 ), 0 ),
		blue : max( min( Number( blue ), 255 ), 0 ),
		alpha: alpha ? max( min( Number( alpha ), 1 ), 0 ) : 1,
	};
};

```
</div>
</details>

[^RgbaValues]: `rgbaStrToValues()` の戻り値の方は，書かなくても OK（つまり `RgbaValues` インターフェースも不要）ですが，`rgbaStrToValues;` する場合, 書いたほうがいいです（eslint で `"@typescript-eslint/explicit-module-boundary-types"`[<sup>gitHub</sup>](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md "typescript-eslint/explicit-module-boundary-types.md at master · typescript-eslint/typescript-eslint") を有効にしている場合は，警告が出ます）．

## 説明
### 正規表現

`'rgba'` もしくは `'rgb'` にマッチする正規表現は，`/rgba?/` なので，`'rgba(*)'`, `'rgb(*)'` の両方にマッチする正規表現は，次のようになります[^rgb]. 

```javascript
/^rgba?\(.*\)$/
```

`'rgb[a](R, G, B[, A])'` の各値は，実数[^percentage]（0 省略可）の正規表現 `/([+-]?\d*\.?\d+)/`[^realNum]でマッチさせます. R, G, B は 0～255, A は 0～1 の範囲ですが，ここでは数値の範囲は限定しません．これを加えると次のようになります．

```javascript
/^rgba?\(([+-]?\d*\.?\d+), ([+-]?\d*\.?\d+), ([+-]?\d*\.?\d+), ([+-]?\d*\.?\d+)\)$/
```

A 値はオプショナルなので，`(?:)`（非格納括弧）で囲って `?` を後ろにつけます．また，カンマと括弧の内側[^space]にはスペースが 0 以上の任意個数含まれるので，`/ */` を `/,/` の前後に入れます. 以下で正規表現は完成です．

```javascript
/^rgba?\( *([+-]?\d*\.?\d+) *, *([+-]?\d*\.?\d+) *, *([+-]?\d*\.?\d+)(?: *, *([+-]?\d*\.?\d+) *)?\)$/
```

### TypeScript / JavaScript

`RegExp.prototype.exec( str )`[<sup>MDN</sup>](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec "RegExp.prototype.exec() - JavaScript | MDN") は，`str` が正規表現にマッチしなかった場合は `null` を，マッチした場合は，正規表現の括弧内にマッチした部分文字列が `[1], ...[n]` に格納されたオブジェクトを返します. なので，`rgb` に指定した文字列が正規表現にマッチしなかった場合は，`if ( ! result ) { return; }` で `undefined` を返すようにしています．

また，TypeScript 4.0.2 の型推論では, この時点で `result` の `[1]`, `[2]`, `[3]`, `[4]` の型は `string` になっていますが，実際には `string|undefined` のはずなので[^arrayProblem]，一応型ガード的に `if ( ! ( red && green && blue ) ) { return; }` を入れています[^isFinite]．

以上のチェックを通過した場合，`red`, `green`, `blue` を 0～255 の数値型に，`alpha` を 0～1 の数値型（ `undefined` の場合は `1` ）に変換した値をオブジェクトに格納して `return` します．

## 補足

※ 2020 年 9 月 17 日追記．

名前付きキャプチャー・グループ [<sup>MDN</sup>](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_Expressions/Groups_and_Ranges "グループと範囲 - JavaScript | MDN") を使えば，

```typescript
	const regExp = concatRegExps( [
		/^rgba?\( *(?<red>[+-]?\d*\.?\d+) *, *(?<green>[+-]?\d*\.?\d+) *, */,
		/(?<blue>[+-]?\d*\.?\d+)(?: *, *(?<alpha>[+-]?\d*\.?\d+) *)?\)$/,
	] );
	const result = regExp.exec( rgba )?.groups;
	if ( ! result ) { return null; }

	const { red, green, blue, alpha } = result;
```
[^optional-chaining]

と記述できますが，2020 年 9 月現在，IE と Android 版 FireFox が未対応であり，Babel での変換も今回のように `RegExp.prototype.source` と併用している場合変換がうまくいかない[^babel]ため，現状では採用していません．

[^optional-chaining]: オプショナル・チェイニング [<sup>MDN</sup>](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Optional_chaining "Optional chaining - JavaScript | MDN") は，2020 年 9 月現在 IE および Android 版の FireFox & Opera が未対応．Babel は，対応していたはず．
[^babel]: Cf. [RegExp の複数行記述，RegExp 内での変数参照がしたいので，正規表現を結合する関数を作った - Qiita #補足](https://qiita.com/juthaDDA/items/f1093b968faa3d810c1c#%E8%A3%9C%E8%B6%B3)．

## あとがき

想定使用例としては，

1. 不透明度を半分にした `'rgba(*)'` を生成する．
2. `RGBA` を `HSLA` や `CMYK` などに変換する別の関数に放り込む．

などを考えています．今回はとりあえず 1. の用途で使う予定です．

より簡単そうな方法としては，`.replace()` で括弧内を取得し，`.split(',')` で各値に分割するという手も考えられますが，個人的には正規表現で文字列全体をマッチさせたほうが安心感があったので，今回の方法をとってみました．

以上です．

[^rgb]: `document.getCompuotedStyle()[someColorProperty]`[<sup>MDN</sup>](https://developer.mozilla.org/ja/docs/Web/API/Window/getComputedStyle "Window.getComputedStyle() - Web API | MDN") の値は（すくなくとも Chrome では）かならず `'rgba(*)'` 形式ですが，`ElementCSSInlineStyle.style[someColorProperty]`[<sup>MDN</sup>](https://developer.mozilla.org/ja/docs/Web/API/ElementCSSInlineStyle/style "ElementCSSInlineStyle.style - Web API | MDN") の値は `'rgb(*)'`形式の場合があります．
[^realNum]: Cf. [数値とマッチする正規表現 - Qiita](https://qiita.com/BlueSilverCat/items/f35f9b03169d0f70818b#%E5%AE%9F%E6%95%B0). 

[^percentage]: `'rgb[a](R, G, B[, A])'` の各値が取りうるデータ型は `<number>` か `<percentage>` です[<sup>MDN</sup>](https://developer.mozilla.org/ja/docs/Web/CSS/color_value#RGB_%E8%89%B2 "<color> - CSS: カスケーディングスタイルシート | MDN")が，`<percentage>` はほとんど見たことがないので，とりあえず無視しています（`CSSStyleDeclaration[someColorProperty]`[<sup>MDN</sup>](https://developer.mozilla.org/ja/docs/Web/API/CSSStyleDeclaration "CSSStyleDeclaration - Web API | MDN") の値も，`rgb[a](<number>, <number>, <number>, <number>)` 形式です）．なお，`rgba` 引数に `<percentage>` を値にもつ `rgb[a](R, G, B[, A])` を入れた場合は，`regExp` にマッチしないため `! result` となって，`return undefined;` となります．

[^space]: 一応括弧の外側にも含みうるので，`/ *\(/` として `regExp.exec( rgba.trim() )` とすれば，さらに厳密になります．

[^arrayProblem]: この問題は，配列一般に当てはまります．[Suggestion: option to include undefined in index signatures · Issue #13778 · microsoft/TypeScript](https://is.gd/JPpEAM) をざっと見たかんじ，仕様どおりでとくに変更される予定もなさそうです．

[^isFinite]: JavaScript では，`isFinite()` を使えばより厳密に，ちゃんと数字と小数点のみの文字列が取れているか確認できます（TypeScript では `isFinite()` に `string` を渡すことはできないので，ちょっと複雑になります）が，そこまでやる必要はないと思います．似たような処理で，もっと正規表現が複雑な場合は，そこまでチェックしたほうがよさそうですが．