【Javascript】SQLのwhere的な文字列で真偽判定【構文解析】【改良版】

#概要
以下の記事でSQLのwhere的な構文（"type1=でんき and type2=ひこう"など）でobjectの真偽判定をするプログラムを書いたのですが、オブジェクト化を一切せずに書いたので、かなり見にくくなってしまいました。
[SQLのwhere的な条件指定によってテーブルから行を抽出【Javascript】【構文解析】](https://qiita.com/shimajiroxyz/items/3350ed9d95fd2e7d32fe)

そこで関数をカプセル化して、コードを簡単にします。

コードとテスト用のファイルは以下に置いてあります。
https://github.com/JiroShimaya/SqlLikeFiltering

実装にあたっては以下の記事を大いに参考にさせていただきました。
[JavaScriptでゆるく学ぶ再帰下降構文解析](https://qiita.com/aql/items/84cead7c028f54e089e0)

#実装
##トークン管理関数
indexの管理から開放されるためのトークン管理関数を用意します。
`this`の扱いが苦手なので、クラスではなくクロージャで実装します。
パブリックな関数は以下の３つです。すべて引数はなしです。

* exist: つぎのindexが存在するかを真偽値で返す。
* read: つぎのindexのtokenの値を返す
* next: つぎのindexのtokenの値を返す。同時にindexを1すすめる。

```Javascript:js/Token.js
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 ;
```

上記式の意味は以下のとおりです。
１つ目のについて、まず、縦棒`|`はその前後の項（例えば`and`と`or`、中括弧`{}`は0回以上の繰り返しの意味です。ここから`expression`は、`factor`一つと、その後ろに0回以上の`and/or factor`が続くものと定義されます。
２つめの式から、`factor`は`term`または丸括弧で囲まれた`expression`であると定義されています。`expression`を構成する`factor`の要素に丸括弧で囲まれた`expression`が登場するので、再帰的に、丸括弧の中身が先に評価されることがわかります。
３つめの式から`term`は文字列を`=`または`!=`で連結したものと定義されています。すなわち`term`が`type1=でんき`のような条件式の最小構成要素に相当している事がわかります。

あとはこの表現を参考にプログラムを書きます。

```Javascript:js/MyParser.js
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】テスト用の関数の自作](https://qiita.com/shimajiroxyz/items/7de4eaa45e45c3aefe63)

```html:index.html
<script type="module" src="js/main.js"></script>
```

```Javascript:js/main.js
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
```