正規表現でマッチしたキャプチャの内容と位置を取得するライブラリ
match-indexという正規表現の補助JavaScriptライブラリを書きました。
例えば、
"ABC ABC"
という文字列から”ABC”という文字列とその位置(index)を取ろうとすると、非常に面倒な書き方をする必要があります。
"ABC ABC".match(/(ABC)/g)
では文字列は取れますが、index
を取ることができません。
これをやるにはmatch
ではなく、g
フラグ付き正規表現とexec
やreplace
を使ってやる必要があります。
これを直感的に行うString.prototype.matchAll
というProposalも存在しています。
今回はこのmatchAll
的なものと、キャプチャした内容と位置を取得出来るmatchCaptureGroupAll
をもったmatch-indexというライブラリを書きました。
const captureGroups = matchCaptureGroupAll("ABC ABC", /(ABC)/);
assert.equal(captureGroups.length, 2);
const [x, y] = captureGroups;
assert.equal(x.text, "ABC");
assert.equal(x.index, 0);
assert.equal(y.text, "ABC");
assert.equal(y.index, 4);
Installation
npm install match-index
でインストールできます。
Usage
match-index
は2つの関数を持ってます。
matchCaptureGroupAll(text, regExp): MatchCaptureGroup
(
と)
で囲まれた内容を配列で返してくれます。
[{
text : "strgin",
index: 5 // 開始位置
}]
配列の中身はtext
とindex
という感じになっています。
// get "ABC" and "EFC that are captured by ( and )
const captureGroups = matchCaptureGroupAll("ABC EFG", /(ABC).*?(EFG)/);
// captureGroups is array of MatchAllGroup
/**
* @typedef {Object} MatchAllGroup
* @property {Array} all
* @property {string} input
* @property {number} index
* @property {MatchCaptureGroup[]} captureGroups
*/
assert(captureGroups.length, 2);
const [x, y] = captureGroups;
assert.equal(x.text, "ABC");
assert.equal(x.index, 0);
assert.equal(y.text, "EFG");
assert.equal(y.index, 4);
matchCaptureGroupAll
は内部的に次に紹介する matchAll
を使っています。
matchAll(text, regExp): MatchAllGroup
一方、matchAll()
はString.prototype.matchAllと似たような感じですが、キャプチャに関する内容をcaptureGroups
に保持する拡張をしています。
const text = 'test1test2';
const regexp = /t(e)(st\d?)/g;
const captureGroups = matchAll(text, regexp);
/**
* @typedef {Object} MatchAllGroup
* @property {Array} all
* @property {string} input
* @property {number} index
* @property {MatchCaptureGroup[]} captureGroups
*/
問題点
これは実装上の問題で、多分正規表現ではなくパーサコンビネータとかで実装すればどうにかできると思いますが、match-indexはネストしたキャプチャを正確に扱えないバグがあります。
例えば、次の例は(st(\d?))
がネストしているため、index
の値がおかしくなっています。
const text = 'test1test2';
const regexp = /t(e)(st(\d?))/g;
const captureGroups = matchAll(text, regexp);
/**
* @typedef {Object} MatchAllGroup
* @property {Array} all
* @property {string} input
* @property {number} index
* @property {MatchCaptureGroup[]} captureGroups
*/
assert.equal(captureGroups.length, 2);
const [test1, test2] = captureGroups;
assert.equal(test1.index, 0);
assert.equal(test1.input, text);
assert.deepEqual(test1.all, ['test1', 'e', 'st1', '1']);
assert.deepEqual(test1.captureGroups, [
{
index: 1,
text: 'e'
}, {
index: 2,
text: 'st1'
}, {
index: -1,// Limitation of capture nest
text: '1'
}
]);
制限として受け止めれば使えますが、いい案が思いつかないので修正するPull Requestを募集しています…
このライブラリはtextlint-rule-preset-JTF-styleを–fixでの自動修正に対応に対応するときに、もっと直感的にマッチしてその位置を取得する方法が欲しくて作りました。
これにより書く効率は上がって、つねにg
フラグで扱われるので、一度マッチしても最後までちゃんと繰り返しマッチするようになって、全てのエラーを出せるようになったので書いてよかったと思います。(textlintのようなLintの特性上、その行にある全部のエラーを出せた方がよいため)
小さなライブラリですが、先ほど書いたようなバグができたりするので正規表現は扱うのが結構難しいです…
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。