0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-07-18

#概要
以下の記事で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すすめる。
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がマッチするかどうかの真偽値を返す関数を作ります。
今回は比較演算子として=!="、条件間の接続詞としてandor、評価の優先度指定のための丸括弧()`に対応させることを目指します。
つまり以下のような式を正しく解釈できることを目指します。

  • type1 = みず and (type1 = でんき or type2=NA)

この構文解析は、バッカスナウア記法で以下のように書けます。

expression = factor, { ("and"|"or"), factor} ;
factor = term | "(", expression, ")" ;
term = string, ("="|"!="), string ;

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

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

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】テスト用の関数の自作

index.html
<script type="module" src="js/main.js"></script>
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
0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?