本稿では、JavaScriptの正規表現の/g
と名前付きグループ(?<name>...)
を併用する小技を紹介します。
名前付きグループとは
名前付きグループとは、正規表現のグループ(...)
に名前をつけ、グループにマッチした文字列をインデックスではなく、その名前で取り出せるようになるものです。
// 名前がついてないグループ
const result1 = 'alice@example.com'.match(/@(.+)/)
console.log(result1[1]) //=> example.com
// 名前付きグループ
const result2 = 'alice@example.com'.match(/@(?<domain>.+)/)
console.log(result2.groups.domain) //=> example.com
/g
と名前付きグループは併用しにくい
/g
と名前付きグループを組み合わせると、match
では.groups
で名前付きグループを参照することができなくなります:
const result1 = '123'.match(/(\d)/g)
console.log(result1) //=> [ '1', '2', '3' ]
const result2 = '123'.match(/(?<digit>\d)/g)
console.log(result2) //=> [ '1', '2', '3' ]
RegExp.prototype.exec
を使うと、/g
と.groups
による参照を両立できますが、コードがごちゃつきます:
const regexp = /(?<digit>\d)/g
let match
const result3 = []
while ((match = regexp.exec('123')) !== null) {
result3.push(match.groups)
}
console.log(result3)
// => [
// { digit: '1' },
// { digit: '2' },
// { digit: '3' }
// ]
ECMAScript 2019で導入された、String.prototype.matchAll
を使うと、コードはきれいになります:
const result4 = '123'.matchAll(/(?<digit>\d)/g)
const result5 = [...result4].map(match => match.groups)
console.log(result5)
// => [
// { digit: '1' },
// { digit: '2' },
// { digit: '3' }
// ]
しかし、ECMAScript 2019に対応したJavaScript実行環境でなければこのメソッドは使えません。
(古い環境で)正規表現/gと名前付きグループを併用する小技
ECMAScript 2019未満の古い環境で、ポリフィルなどなしに、String.prototype.matchAll
のような比較的ととのった見た目のコードで、正規表現/g
と名前付きグループを両立するにはどうしたらいいでしょうか。
少しトリッキーですが、String.prototype.replace
を使います:
const string = '123'
const regexp = /(?<digit>\d)/g
const result = []
string.replace(regexp, (...args) => result.push(args.pop()))
console.log(result)
// => [
// { digit: '1' },
// { digit: '2' },
// { digit: '3' }
// ]
String.prototype.replace
は第2引数にコールバック関数を渡すことができます。コールバック関数の引数には、マッチした文字列の情報がいろいろ渡されますが、最後の引数がgroups
なので、最後のその引数.pop()
すると、名前付きグループのオブジェクトが取れるというわけです。
おわりに
今回紹介した小技はある種のハックなので、String.prototype.matchAll
が使えるようにできないかを検討するのが先です。