実行サンプル
var condition = [
['dog', 'fox', '-cow'],
['cat'],
['pig'],
]; // 意味は (dog AND fox AND !cow) OR cat OR pig
var matcher = compile(condition);
matcher.test("The quick brown fox jumps over the lazy dog.")); // true
compile関数の中身
function compile(cond) {
var joinAnd = function (arr) { return '^(?=[\\s\\S]*' + arr.join(')(?=[\\s\\S]*') + ')'; };
var joinOr = function (arr) { return '(?:' + arr.join('|') + ')'; };
var escape = function (str) { return str.replace(/(?=[(){}\[\].*\\^$?])/, '\\'); };
var rx = joinOr(cond.map(function(inner) { return joinAnd(inner.map(escape)) }));
rx = rx.replace(/=\[\\s\\S\]\*-/g, '![\\s\\S]*');
return new RegExp(rx);
}
解説・補足
- 基本的に以下のような意味の正規表現を作ってるだけです。
/(^(?=.*dog)(?=.*fox)(?!.*cow)|cat|pig)/
-
(?=)
で先読みしているためAND条件部分はどれが先に来てもマッチできます。 - 先読みの中に
.*
が入っており対象文字列の先頭で1回試行してゼロ幅マッチしてくれれば良いようにしているため^
を置いています。これを忘れると対象がN文字だった時N倍の時間がかかります。 - 正規表現中の
.
は改行にマッチしないので[\s\S]
で代替しています。 - 当然ですが行っているescape処理を省けば条件に正規表現が使えます。
- 特定文字の前に「\」を置くだけのエスケープなら先読みを使うことで、キャプチャせず単純なreplaceで表現できます。
-
new RegExp(rx, 'i');
にすれば大文字小文字を無視します。 - 適当にNode.jsでベンチマークをとった所、コンパイル部分のコストを無視しても、ループして地味に
indexOf
する方が倍くらい早かったです。 - これを書いている時にNOT機能を思いついて後付けしています。
使い所
コードが少ない割に十分な機能と小回りが効くようにしてあるため、ユーザスクリプトに組み込んで使っています。
こんな感じで、見たくないコメントなどをあぼーんしたりするのに使えます。
(ユーザスクリプトで使う場合エスケープ処理は無くしてしまった方が使いやすそうです)
Array.prototype.slice.call(document.getElementsByClassName('comment'))
.filter(function(c) { return matcher.test(c.textContent) })
.map(function(c) { c.className += ' hide'; });
逆にGoogle検索で (A|B) C D
とするように、複数の対象の中から目的のものを探し出すような使い方をする場合、joinAnd
とjoinOr
の呼び出している部分を交換します。
(恐らくタイトルから連想される動きはこちらだと思います)
var rx = joinAnd(cond.map(function(inner) { return joinOr(inner.map(escape)) }));