JavaScript
正規表現
ECMAScript

あなたの知っている正規表現はもう古い! 正規表現の新常識(ES2018編)

2018年1月23日から25日にかけて、TC39の第62回ミーティングが行われました。TC39のミーティングでは、提案されているECMAScriptの新機能 (proposal) について審議し、各proposalのステージの移動を決定します。
今回のミーティングでは正規表現に関する幾つかのproposalがStage 4になりました。Stage 4になったproposalはES2018に組み込まれ、JavaScript (ECMAScript) に正式採用ということになります。

この記事では、JavaScriptに追加された正規表現の4つの新機能を紹介します。

s (dotAll) flag for regular expressions

Proposal: https://github.com/tc39/proposal-regexp-dotall-flag

正規表現の新たなフラグとしてsが追加されました。フラグというのは、大文字小文字を無視するiや繰り返しマッチを意味するgなどのことです。

sフラグの効果は、.が真に全ての文字にマッチするようになるというものです。

というのも、.は全ての文字にマッチする記号として知られていますが、実は本当は全ての文字にマッチするわけではありません。改行文字(CR, LF, U+2028, U+2029)にマッチしないからです。

console.log(/foo.bar/.test(`foo
bar`)); // false

ES2018で新しく導入されたsフラグが付いている正規表現は、.が真に全ての文字にマッチするようになります。

console.log(/foo.bar/s.test(`foo
bar`)); // true

これにより、全ての文字を[\s\S][^]で表すといったテクニックは不要になります。(改行にはマッチしない.も使いたいというような場合は別ですが。)

RegExp Named Capture Groups

Proposal: https://github.com/tc39/proposal-regexp-named-groups

これはキャプチャグループに名前を付けられるようになるというものです。

キャプチャグループというのは、正規表現で使える( ... )という構文のことです。特に、グループにマッチした部分文字列はmatchなどのメソッドで取得できます。

const str = 'foo bar';

const [, left, right] = str.match(/^(\w+)\s+(\w+)$/);

console.log(left); // 'foo'
console.log(right); // 'bar'

この例では、matchの返り値である配列の1番目をleft, 2番目をrightとして表示しています。このように、従来キャプチャグループは番号で識別されました。

番号でグループを区別するのは分かりにくいということで、ES2018ではグループに名前を付けることができるようになりました。そのためには、(?<name> ... )という記法を用います。nameというところにこのグループに付けたい名前を入れてください。

const str = 'foo bar';
const {
  groups: {
    left,
    right,
  },
} = str.match(/^(?<left>\w+)\s+(?<right>\w+)$/);

console.log(left); // 'foo'
console.log(right); // 'bar'

matchメソッドの場合、返り値は配列ですが、その配列にgroupsというプロパティが追加されています。これはオブジェクトであり、各グループにマッチした文字列が入っています。

また、グループにマッチした文字列は正規表現中で\1のように参照することができました。名前付きグループの場合は\k<name>という構文で可能になっています。

console.log(/^(?<part>\w+)\k<part>$/.test('foofoo')); // true

String.prototype.replaceメソッドで$1などとしていたのも同様に$<name>での参照が可能になっています。

RegExp Lookbehind Assertions

Proposal: https://github.com/tc39/proposal-regexp-lookbehind

これは、正規表現に後読み機能が追加されるというものです。

正規表現は従来から先読み機能がありましたが、その逆向きバージョンとなっています。構文は(?<= ...)です。

次の例は、$(ドル記号)に続く数字の列にマッチするという典型的な正規表現です。

const result = '123$456 $789 $ 000'.match(/(?<=\$)\d+/g);

console.log(result); // ['456', '789']

(?<=\$)という部分で後読み機能を使用しています。これは、(?<=\$)がある位置は$が直前にあるような位置でなければならないという意味になります。そして、そのような位置の直後に\d+、すなわち数字の列があればマッチします。ポイントは、上の例では123000はこの正規表現にマッチしないということです。なぜなら、それらの場合は(?<=\$)にあたる位置の直前に$が無いからです。

後読み機能のいいところは、後読み部分はマッチした文字列に含まれない点です。後読み機能を使わずに上の例と同じことをしたい場合は、例えば次のように$をマッチ文字列に含めたあとで除去する必要があります。

const result = '123$456 $789 $ 000'.match(/\$\d+/g)
.map(x=> x.slice(1));

console.log(result); // ['456', '789']

ちなみに、後読みには否定形もあります。それは(?<! ...)です。

Unicode property escapes in regular expressions

Proposal: https://github.com/tc39/proposal-regexp-unicode-property-escapes

これは、Unicodeで文字に付与されているプロパティを正規表現で利用できるという機能です。構文は\p{PropertyName=PropertyValue}です。

Unicodeでは、各文字にさまざまなプロパティが付与されています。一例として、General_Categoryというプロパティは文字の分類を表します。例えば、Decimal_Numberは0〜9の数字を表す値です。General_CategoryがDecimal_Numberであるような文字にマッチする表現は\p{General_Category=Decimal_Number}と書けます。

const re = /^\p{General_Category=Decimal_Number}+$/u;

console.log(re.test('123')); // true
console.log(re.test('123foo456')); // false

ここで特筆すべきは、Decimal_Numberというカテゴリが与えられているのは半角数字の0から9だけではないということです。例えば全角数字や黒板太字などもこのカテゴリに入ります。

const re = /^\p{General_Category=Decimal_Number}+$/u;

console.log(re.test('12345𝟞𝟕')); // true

なお、この\p{...}は正規表現のuフラグが有効になっていないと使えないので注意してください。

ちなみに、上の例の場合General_Categoryはgcという略称があり、Decimal_NumberはNdという略称があるので次のようにすることもできます。

const re = /^\p{gc=Nd}+$/u;

console.log(re.test('12345𝟞𝟕')); // true

また、General_Categoryはよく使われるので省略できます。

const re = /^\p{Nd}+$/u;

console.log(re.test('12345𝟞𝟕')); // true

また、Unicodeにはバイナリプロパティという種類のプロパティもあります。これはそのプロパティに属するか属さないかの2種類しかないプロパティです。この場合は\p{プロパティ名=値}ではなく\p{プロパティ名}とします。例えばLowercaseというプロパティは小文字を表すプロパティです。

const re = /^\p{Lowercase}+$/u;

console.log(re.test('foobar')); // true
console.log(re.test('Foobar')); // false
console.log(re.test('ελληνικά')); // true

この新機能は、Unicodeに存在する各種のプロパティを知っていなければ使えないので使用難易度が高いものの、今まで簡単に書けなかった正規表現を簡単に書くことができます。特に、Unicodeに収録されている様々な種類の文字に簡単に対応できるようになります。例えば最後の例に見えるように、\p{Lowercase}はラテン文字だけでなくギリシャ文字などの小文字にも対応しており、[a-z]とは異なることが分かります。

ブラウザ・処理系の対応

これらの新機能はいつから使えるのでしょうか。詳しいことはECMAScript 2016+ compatibility tableを参照してもらいたいのですが、

  • Chromeはもう対応済(Chrome 64以上)
  • Firefoxはまだ
  • Edgeもまだ
  • Safariもまだ(後読み以外はTPにはある)
  • node.jsはフラグが必要(8.x以上)

という感じです。

まとめ

ES2018で追加された正規表現の新機能を紹介しました。

古くなったのは[\s\S]や名前のないキャプチャグループとかそのへんです。