RegExp v フラグと集合記法、文字列のプロパティ

公開日 · タグ: ECMAScript

JavaScript は ECMAScript 3 (1999) から正規表現をサポートしています。16 年後、ES2015 では、Unicode モード (u フラグ)sticky モード (y フラグ)、そしてRegExp.prototype.flags ゲッターが導入されました。さらに 3 年後、ES2018 では、dotAll モード (s フラグ)後読みアサーション名前付きキャプチャグループ、そしてUnicode 文字プロパティエスケープが導入されました。そして ES2020 では、String.prototype.matchAll によって正規表現の操作がより簡単になりました。JavaScript の正規表現は大きな進歩を遂げており、現在も改善が続けられています。

その最新の例が、新しい unicodeSets モードで、v フラグを使って有効化します。この新しいモードは、次の機能を含む拡張文字クラスのサポートを有効にします。

この記事では、これらの各機能について詳しく説明します。まずは、この新しいフラグの使い方から説明します。

const re = //v;

v フラグは、既存の正規表現フラグと組み合わせることができますが、1 つだけ注目すべき例外があります。v フラグは、u フラグの良い部分をすべて有効にしますが、追加の機能と改善も加わっています。その一部は、u フラグとの後方互換性がありません。重要な点として、v は補完的なモードではなく、u とは完全に別のモードです。このため、vu のフラグを組み合わせることはできません。同じ正規表現で両方のフラグを使用しようとするとエラーになります。有効なオプションは、u を使用するか、v を使用するか、uv も使用しないかのいずれかです。しかし、v が最も機能が充実しているオプションであるため、選択は容易です…。

それでは、新しい機能を詳しく見ていきましょう!

文字列の Unicode プロパティ #

Unicode 標準では、すべての記号にさまざまなプロパティとプロパティ値が割り当てられています。たとえば、ギリシャ文字で使用される記号のセットを取得するには、Unicode データベースで Script_Extensions プロパティ値に Greek が含まれる記号を検索します。

ES2018 の Unicode 文字プロパティエスケープにより、これらの Unicode 文字プロパティに ECMAScript の正規表現でネイティブにアクセスできるようになります。たとえば、パターン \p{Script_Extensions=Greek} は、ギリシャ文字で使用されるすべての記号に一致します。

const regexGreekSymbol = /\p{Script_Extensions=Greek}/u;
regexGreekSymbol.test('π');
// → true

定義上、Unicode 文字プロパティはコードポイントのセットに展開されるため、個別に一致するコードポイントを含む文字クラスとしてトランスパイルできます。たとえば、\p{ASCII_Hex_Digit}[0-9A-Fa-f] と同等です。一度に 1 つの Unicode 文字/コードポイントのみに一致します。状況によっては、これでは不十分です。

// Unicode defines a character property named “Emoji”.
const re = /^\p{Emoji}$/u;

// Match an emoji that consists of just 1 code point:
re.test('⚽'); // '\u26BD'
// → true ✅

// Match an emoji that consists of multiple code points:
re.test('👨🏾‍⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → false ❌

上記の例では、正規表現は 👨🏾‍⚕️ の絵文字に一致しません。これは、複数のコードポイントで構成されているためであり、Emoji は Unicode の文字プロパティです。

幸いなことに、Unicode 標準では、いくつかの文字列のプロパティも定義されています。このようなプロパティは、それぞれ 1 つ以上のコードポイントを含む文字列のセットに展開されます。正規表現では、文字列のプロパティは代替文字列のセットに変換されます。これを説明するために、文字列 'a''b''c''W''xy'、および 'xyz' に適用される Unicode プロパティを想像してください。このプロパティは、次のいずれかの正規表現パターン (代替を使用) に変換されます: xyz|xy|a|b|c|W または xyz|xy|[a-cW]。(最長の文字列が最初になるように、'xy' のような接頭辞が 'xyz' のような長い文字列を隠さないようにします)。既存の Unicode プロパティエスケープとは異なり、このパターンは複数文字の文字列に一致できます。以下は、文字列のプロパティを使用している例です。

const re = /^\p{RGI_Emoji}$/v;

// Match an emoji that consists of just 1 code point:
re.test('⚽'); // '\u26BD'
// → true ✅

// Match an emoji that consists of multiple code points:
re.test('👨🏾‍⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → true ✅

このコードスニペットは、Unicode が「一般的な交換に推奨されるすべての有効な絵文字 (文字とシーケンス) のサブセット」として定義している文字列のプロパティ RGI_Emoji を参照しています。これで、絵文字が内部的にいくつのコードポイントで構成されているかに関係なく、絵文字に一致させることができるようになりました。

v フラグは、次の文字列の Unicode プロパティのサポートを最初から有効にします。

  • Basic_Emoji
  • Emoji_Keycap_Sequence
  • RGI_Emoji_Modifier_Sequence
  • RGI_Emoji_Flag_Sequence
  • RGI_Emoji_Tag_Sequence
  • RGI_Emoji_ZWJ_Sequence
  • RGI_Emoji

このサポートされているプロパティのリストは、Unicode 標準で文字列の追加プロパティが定義されるにつれて、将来的に増える可能性があります。現在のすべての文字列のプロパティは絵文字関連ですが、将来の文字列のプロパティはまったく異なるユースケースに対応する可能性があります。

注: 文字列のプロパティは現在、新しい v フラグによってゲートされていますが、最終的には u モードでも利用できるようにする予定です

集合記法 + 文字列リテラル構文 #

\p{…} エスケープ (文字プロパティであろうと新しい文字列のプロパティであろうと) を操作する場合、差/減算または積演算を実行できると便利です。v フラグを使用すると、文字クラスをネストできるようになり、これらの集合演算を、隣接する先読みまたは後読みアサーションや、計算された範囲を表す長い文字クラスではなく、内部で実行できるようになります。

-- による差/減算 #

構文 A--B を使用して、A に含まれているが B には含まれていない文字列 (つまり、差/減算) に一致させることができます。

たとえば、文字 π を除くすべてのギリシャ文字に一致させたい場合はどうすればよいでしょうか?集合記法を使用すると、これを簡単に解決できます。

/[\p{Script_Extensions=Greek}--π]/v.test('π'); // → false

差/減算に -- を使用することで、正規表現エンジンが複雑な処理を実行し、コードの可読性と保守性を維持します。

単一の文字ではなく、文字セット αβ、および γ を減算したい場合はどうすればよいでしょうか?問題ありません。ネストされた文字クラスを使用し、その内容を減算できます。

/[\p{Script_Extensions=Greek}--[αβγ]]/v.test('α'); // → false
/[\p{Script_Extensions=Greek}--[α-γ]]/v.test('β'); // → false

別の例として、後で ASCII 数字に変換するために、ASCII 以外の数字に一致させることもできます。

/[\p{Decimal_Number}--[0-9]]/v.test('𑜹'); // → true
/[\p{Decimal_Number}--[0-9]]/v.test('4'); // → false

集合記法は、新しい文字列のプロパティでも使用できます。

// Note: 🏴󠁧󠁢󠁳󠁣󠁴󠁿 consists of 7 code points.

/^\p{RGI_Emoji_Tag_Sequence}$/v.test('🏴󠁧󠁢󠁳󠁣󠁴󠁿'); // → true
/^[\p{RGI_Emoji_Tag_Sequence}--\q{🏴󠁧󠁢󠁳󠁣󠁴󠁿}]$/v.test('🏴󠁧󠁢󠁳󠁣󠁴󠁿'); // → false

この例は、スコットランドの旗を除く、任意の RGI 絵文字タグシーケンスに一致します。文字クラス内の文字列リテラルのための別の新しい構文である \q{…} の使用に注目してください。たとえば、\q{a|bc|def} は文字列 abc、および def に一致します。\q{…} がないと、ハードコードされた複数文字の文字列を減算することはできません。

&& による積集合 #

A&&B 構文は、AB の両方に含まれる文字列 (つまり、積集合) に一致します。これにより、ギリシャ文字に一致させるようなことができます。

const re = /[\p{Script_Extensions=Greek}&&\p{Letter}]/v;
// U+03C0 GREEK SMALL LETTER PI
re.test('π'); // → true
// U+1018A GREEK ZERO SIGN
re.test('𐆊'); // → false

すべての ASCII 空白に一致させる

const re = /[\p{White_Space}&&\p{ASCII}]/v;
re.test('\n'); // → true
re.test('\u2028'); // → false

または、すべてのモンゴル数字に一致させる

const re = /[\p{Script_Extensions=Mongolian}&&\p{Number}]/v;
// U+1817 MONGOLIAN DIGIT SEVEN
re.test('᠗'); // → true
// U+1834 MONGOLIAN LETTER CHA
re.test('ᠴ'); // → false

和集合 #

A または B に含まれる文字列に一致させることは、以前は [\p{Letter}\p{Number}] のような文字クラスを使用することで、単一文字の文字列で可能でした。v フラグを使用すると、この機能がより強力になります。これは、文字列のプロパティや文字列リテラルとも組み合わせることができるようになったためです。

const re = /^[\p{Emoji_Keycap_Sequence}\p{ASCII}\q{🇧🇪|abc}xyz0-9]$/v;

re.test('4️⃣'); // → true
re.test('_'); // → true
re.test('🇧🇪'); // → true
re.test('abc'); // → true
re.test('x'); // → true
re.test('4'); // → true

このパターンの文字クラスは、次を組み合わせています。

  • 文字列のプロパティ (\p{Emoji_Keycap_Sequence})
  • 文字プロパティ (\p{ASCII})
  • 複数コードポイント文字列 🇧🇪 および abc の文字列リテラル構文
  • 単一の文字 xy、および z の古典的な文字クラス構文
  • 0 から 9 までの文字範囲の古典的な文字クラス構文

別の例として、2 文字の ISO コード (RGI_Emoji_Flag_Sequence) としてエンコードされているか、特殊な場合分けされたタグシーケンス (RGI_Emoji_Tag_Sequence) としてエンコードされているかに関係なく、一般的に使用されるすべての旗の絵文字に一致させることができます。

const reFlag = /[\p{RGI_Emoji_Flag_Sequence}\p{RGI_Emoji_Tag_Sequence}]/v;
// A flag sequence, consisting of 2 code points (flag of Belgium):
reFlag.test('🇧🇪'); // → true
// A tag sequence, consisting of 7 code points (flag of England):
reFlag.test('🏴󠁧󠁢󠁥󠁮󠁧󠁿'); // → true
// A flag sequence, consisting of 2 code points (flag of Switzerland):
reFlag.test('🇨🇭'); // → true
// A tag sequence, consisting of 7 code points (flag of Wales):
reFlag.test('🏴󠁧󠁢󠁷󠁬󠁳󠁿'); // → true

改善された大文字小文字を区別しないマッチング #

ES2015 u フラグには、大文字と小文字を区別しないマッチングの紛らわしい動作という問題があります。次の 2 つの正規表現について考えてみましょう。

const re1 = /\p{Lowercase_Letter}/giu;
const re2 = /[^\P{Lowercase_Letter}]/giu;

最初のパターンは、すべての小文字に一致します。2 番目のパターンは、小文字を除くすべての文字に一致させるために \p の代わりに \P を使用していますが、否定された文字クラス ([^…]) でラップされています。両方の正規表現は、i フラグ (ignoreCase) を設定することで、大文字と小文字を区別しないようにされています。

直感的には、両方の正規表現が同じように動作することを期待するかもしれません。実際には、動作は大きく異なります。

const re1 = /\p{Lowercase_Letter}/giu;
const re2 = /[^\P{Lowercase_Letter}]/giu;

const string = 'aAbBcC4#';

string.replaceAll(re1, 'X');
// → 'XXXXXX4#'

string.replaceAll(re2, 'X');
// → 'aAbBcC4#''

新しい v フラグは、より驚きのない動作をします。u フラグの代わりに v フラグを使用すると、両方のパターンが同じように動作します。

const re1 = /\p{Lowercase_Letter}/giv;
const re2 = /[^\P{Lowercase_Letter}]/giv;

const string = 'aAbBcC4#';

string.replaceAll(re1, 'X');
// → 'XXXXXX4#'

string.replaceAll(re2, 'X');
// → 'XXXXXX4#'

より一般的には、v フラグを使用すると、i フラグが設定されているかどうかにかかわらず、[^\p{X}][\P{X}]\P{X} および [^\P{X}][\p{X}]\p{X} になります。

さらに詳しく #

提案リポジトリには、これらの機能とその設計上の決定に関する詳細と背景が記載されています。

これらの JavaScript 機能に関する作業の一環として、私たちは ECMAScript への仕様変更を提案するだけでなく、「文字列のプロパティ」の定義を Unicode UTS#18 にアップストリームして、他のプログラミング言語が同様の機能を統一された方法で実装できるようにしました。また、pattern 属性でもこれらの新機能を有効にすることを目指して、HTML 標準への変更も提案しています

RegExp v フラグのサポート #

V8 v11.0 (Chrome 110) では、--harmony-regexp-unicode-sets フラグを介してこの新機能の実験的サポートが提供されています。V8 v12.0 (Chrome 112) では、新しい機能がデフォルトで有効になっています。Babel は v フラグのトランスパイルもサポートしています。— Babel REPL でこの記事の例を試してみてください! 下のサポートテーブルには、更新を購読できるトラッキング issue へのリンクが記載されています。