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


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

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

が返ってくる関数です.

コード

※ 2020 年 9 月 17 日,RegExp の複数行記述,RegExp 内での変数参照がしたいので,正規表現を結合する関数を作った - Qiita の内容を反映させ更新.

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,
    };
};

1

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,
    };
};


説明

正規表現

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

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

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

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

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

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

TypeScript / JavaScript

RegExp.prototype.exec( str )MDN は,str が正規表現にマッチしなかった場合は null を,マッチした場合は,正規表現の括弧内にマッチした部分文字列が [1], ...[n] に格納されたオブジェクトを返します. なので,rgb に指定した文字列が正規表現にマッチしなかった場合は,if ( ! result ) { return; }undefined を返すようにしています.

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

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

補足

※ 2020 年 9 月 17 日追記.

名前付きキャプチャー・グループ MDN を使えば,

    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;

8

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

あとがき

想定使用例としては,

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

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

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

以上です.


  1. rgbaStrToValues() の戻り値の方は,書かなくても OK(つまり RgbaValues インターフェースも不要)ですが,rgbaStrToValues; する場合, 書いたほうがいいです(eslint で "@typescript-eslint/explicit-module-boundary-types"gitHub を有効にしている場合は,警告が出ます). 

  2. document.getCompuotedStyle()[someColorProperty]MDN の値は(すくなくとも Chrome では)かならず 'rgba(*)' 形式ですが,ElementCSSInlineStyle.style[someColorProperty]MDN の値は 'rgb(*)'形式の場合があります. 

  3. 'rgb[a](R, G, B[, A])' の各値が取りうるデータ型は <number><percentage> ですMDNが,<percentage> はほとんど見たことがないので,とりあえず無視しています(CSSStyleDeclaration[someColorProperty]MDN の値も,rgb[a](<number>, <number>, <number>, <number>) 形式です).なお,rgba 引数に <percentage> を値にもつ rgb[a](R, G, B[, A]) を入れた場合は,regExp にマッチしないため ! result となって,return undefined; となります. 

  4. Cf. 数値とマッチする正規表現 - Qiita.  

  5. 一応括弧の外側にも含みうるので,/ *\(/ として regExp.exec( rgba.trim() ) とすれば,さらに厳密になります. 

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

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

  8. オプショナル・チェイニング MDN は,2020 年 9 月現在 IE および Android 版の FireFox & Opera が未対応.Babel は,対応していたはず. 

  9. Cf. RegExp の複数行記述,RegExp 内での変数参照がしたいので,正規表現を結合する関数を作った - Qiita #補足.