#概要
以下の記事でSQLのwhere的な構文("type1=でんき and type2=ひこう"など)でobjectの真偽判定をするプログラムを書いたのですが、オブジェクト化を一切せずに書いたので、かなり見にくくなってしまいました。
SQLのwhere的な条件指定によってテーブルから行を抽出【Javascript】【構文解析】
そこで関数をカプセル化して、コードを簡単にします。
コードとテスト用のファイルは以下に置いてあります。
https://github.com/JiroShimaya/SqlLikeFiltering
実装にあたっては以下の記事を大いに参考にさせていただきました。
JavaScriptでゆるく学ぶ再帰下降構文解析
#実装
##トークン管理関数
indexの管理から開放されるためのトークン管理関数を用意します。
this
の扱いが苦手なので、クラスではなくクロージャで実装します。
パブリックな関数は以下の3つです。すべて引数はなしです。
- exist: つぎのindexが存在するかを真偽値で返す。
- read: つぎのindexのtokenの値を返す
- next: つぎのindexのtokenの値を返す。同時にindexを1すすめる。
function Token(tokens){
const _this = {}
_this.idx = 0;
_this.tokens = tokens;
_this.exist = function(){
if(_this.idx<0){
return false;
}
if(_this.tokens.length <= _this.idx){
return false;
}
return true;
}
_this.read = function(){
return _this.tokens[_this.idx]
}
_this.next = function(){
return _this.tokens[_this.idx++]
}
return {
exist: _this.exist,
read: _this.read,
next: _this.next
}
}
export { Token };
##パーサー
where構文を解釈してobjectがマッチするかどうかの真偽値を返す関数を作ります。
今回は比較演算子として=
、!="、条件間の接続詞として
and、
or、評価の優先度指定のための丸括弧
()`に対応させることを目指します。
つまり以下のような式を正しく解釈できることを目指します。
- type1 = みず and (type1 = でんき or type2=NA)
この構文解析は、バッカスナウア記法で以下のように書けます。
expression = factor, { ("and"|"or"), factor} ;
factor = term | "(", expression, ")" ;
term = string, ("="|"!="), string ;
上記式の意味は以下のとおりです。
1つ目のについて、まず、縦棒|
はその前後の項(例えばand
とor
、中括弧{}
は0回以上の繰り返しの意味です。ここからexpression
は、factor
一つと、その後ろに0回以上のand/or factor
が続くものと定義されます。
2つめの式から、factor
はterm
または丸括弧で囲まれたexpression
であると定義されています。expression
を構成するfactor
の要素に丸括弧で囲まれたexpression
が登場するので、再帰的に、丸括弧の中身が先に評価されることがわかります。
3つめの式からterm
は文字列を=
または!=
で連結したものと定義されています。すなわちterm
がtype1=でんき
のような条件式の最小構成要素に相当している事がわかります。
あとはこの表現を参考にプログラムを書きます。
import { Token } from "./Token.js";
/*
* expression = factor, { ("and"|"or"), factor} ;
* factor = term | "(", expression, ")" ;
* term = string, ("="|"!="), string ;
*/
function MyParser(){
const _this = {}
_this.exec = function(obj, str){
//console.log(obj,str);
if(!str){
console.log("Syntax Error: null input");
return;
}
_this.obj = obj;
let tokens = _this.tokenize(str);
let lexer = new Token(tokens);
let result = _this.expression(lexer);
//alert(lexer.read());
if(lexer.exist()){
console.log("Syntax Error: unexpected character =", lexer.read());
}
return result;
}
_this.tokenize = function(str){
return str.replace(/!=|=|\(|\)/g," $& ").trim().split(/\s+/);
}
_this.expression = function(lexer) {
let result = _this.factor(lexer);
while(lexer.read() === "and" || lexer.read() === "or"){
let next = lexer.next();
let r = _this.factor(lexer);
if(next === "and"){
result = (result && r);
}else{
result = (result || r);
}
}
return result;
}
_this.factor = function(lexer){
if(lexer.read() === "("){
lexer.next();
let result = _this.expression(lexer);
if(lexer.read() === ")"){
lexer.next();
}
else{
console.log("Syntax Error: unexpected character =", lexer.read());
}
return result;
}
else{
return _this.term(lexer);
}
}
_this.term = function(lexer){
let key = lexer.next();
let operator = lexer.next();
let value = lexer.next();
if(operator === "="){
return _this.obj[key] === value;
}
else if(operator === "!="){
return _this.obj[key] !== value;
}
else{
console.log("Syntax Error: unexpected character =", lexer.read());
}
}
return {
exec: _this.exec
}
}
export { MyParser }
#テスト
MyParserの出力をチェックします。
ピカチュウのタイプをいくつかのWhere構文で判定してみます。
テスト用の関数は以下のTester.testAll
を使います。
【Javascript】テスト用の関数の自作
<script type="module" src="js/main.js"></script>
import { MyParser } from "./MyParser.js";
import { Tester } from "./module/Tester/Tester.js";
let obj = {"name":"ピカチュウ","type1":"でんき","type2":"NA"}
let p = new MyParser();
let tester = new Tester();
const data = [
[obj, "type1=でんき", true],
[obj, "type1 = みず", false],
[obj, "type1!=でんき", false],
[obj, "type1 != みず", true],
[obj, "type1 = みず or type1 = でんき", true],
[obj, "type1 = みず and type1 = でんき", false],
[obj, "type1 = みず and type1 = でんき or type2=NA", true],
[obj, "type1 = みず and (type1 = でんき or type2=NA)",false]
]
tester.testAll(p.exec, data);
出力は以下です。うまくできているようです。
OK: args= [{"name":"ピカチュウ","type1":"でんき","type2":"NA"},"type1=でんき"] true = true
OK: args= [{"name":"ピカチュウ","type1":"でんき","type2":"NA"},"type1 = みず"] false = false
OK: args= [{"name":"ピカチュウ","type1":"でんき","type2":"NA"},"type1!=でんき"] false = false
OK: args= [{"name":"ピカチュウ","type1":"でんき","type2":"NA"},"type1 != みず"] true = true
OK: args= [{"name":"ピカチュウ","type1":"でんき","type2":"NA"},"type1 = みず or type1 = でんき"] true = true
OK: args= [{"name":"ピカチュウ","type1":"でんき","type2":"NA"},"type1 = みず and type1 = でんき"] false = false
OK: args= [{"name":"ピカチュウ","type1":"でんき","type2":"NA"},"type1 = みず and type1 = でんき or type2=NA"] true = true
OK: args= [{"name":"ピカチュウ","type1":"でんき","type2":"NA"},"type1 = みず and (type1 = でんき or type2=NA)"] false = false