JavaScript で数式をパースする
最終更新:2009/08/07
●数式をパースするスクリプト
JavaScript での実装例。パースするのみで計算するわけではない。
001: // arith_exp_parser.js 002: // Parser for the arithmetic expression 003: // KAKU PROJECT (2009) 004: 005: // Syntax of the arithmetic expression 006: // exp1 ::= exp2 ( op1 exp2 )* 007: // exp2 ::= exp3 ( op2 exp3 )* 008: // exp3 ::= '(' exp1 ')' | op1 exp2 | num 009: // num ::= ( '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' )+ 010: // op1 ::= '+' | '-' 011: // op2 ::= '*' | '/' 012: 013: 014: // Usage: 015: //var p = new AEParser("1234+9876-789*67"); 016: //var n = p.parse(); 017: 018: var debugItems = {}; 019: debugItems['tokenizer.readnum']=0; 020: debugItems['parser.readexp3']=0; 021: debugItems['parser.readexp2']=0; 022: debugItems['parser.readexp1']=0; 023: debugItems['tokenizer.nexttoken']=0; 024: debugItems['tokenizer.accepttoken']=0; 025: 026: function kdebug (d,x) { 027: if (debugItems[d] == 1) { 028: // WScript.Echo(d + ":", x); 029: } 030: } 031: 032: // AETokenクラス 033: // インスタンスプロパティ 034: // String value トークンの値 035: // インスタンスメソッド 036: // boolean isNum() 数字トークンか 037: // boolean isOP1() OP1 演算子トークン(+,-)か 038: // boolean isOP2() OP2 演算子トークン(*,/)か 039: // boolean isLP() 左括弧トークンか 040: // boolean isRP() 右括弧トークンか 041: // boolean isEOT() EOTトークンか 042: // String toString() トークンの文字列表現 043: 044: // インスタンスメソッド 045: function AEToken_isNUM () { 046: return this.type == AEToken.NUM; 047: } 048: function AEToken_isOP1 () { 049: return this.type == AEToken.OP1; 050: } 051: function AEToken_isOP2 () { 052: return this.type == AEToken.OP2; 053: } 054: function AEToken_isLP () { 055: return this.type == AEToken.LP; 056: } 057: function AEToken_isRP () { 058: return this.type == AEToken.RP; 059: } 060: function AEToken_isEOT () { 061: return this.type == AEToken.EOT; 062: } 063: function AEToken_toString () { 064: if (this.isNUM()) { 065: return "[NUM:" + this.value + "]"; 066: } 067: if (this.isLP()) { 068: return "[LP]"; 069: } 070: if (this.isRP()) { 071: return "[RP]"; 072: } 073: if (this.isOP1()) { 074: return "[OP1:" + String.fromCharCode(this.value) + "]"; 075: } 076: if (this.isOP2()) { 077: return "[OP2:" + String.fromCharCode(this.value) + "]"; 078: } 079: if (this.isEOT()) { 080: return "[EOT]"; 081: } 082: } 083: 084: // コンストラクタ 085: function AEToken (type, value) { 086: this.type = type; 087: this.value = value; 088: 089: this.isNUM = AEToken_isNUM; 090: this.isOP1 = AEToken_isOP1; 091: this.isOP2 = AEToken_isOP2; 092: this.isLP = AEToken_isLP; 093: this.isRP = AEToken_isRP; 094: this.isEOT = AEToken_isEOT; 095: this.toString = AEToken_toString; 096: } 097: // クラスプロパティ 098: AEToken.NUM = 0; 099: AEToken.OP1 = 1; 100: AEToken.OP2 = 2; 101: AEToken.LP = 3; 102: AEToken.RP = 4; 103: AEToken.EOT = 99; 104: 105: // AETokenizer クラス 106: // コンストラクタ 107: // AETokenizer(String text) 108: // インスタンスメソッド 109: // AEToken nextToken () 110: // 現在のトークンを取得する。acceptToken するまで後続のトークンを取得しにいかない。 111: // 戻り値 null --- トークン取得失敗 112: // これ以上トークンを取得できない場合、EOT トークンを返す。 113: // void acceptToken () 114: // 現在のトークンを破棄する。次に nextToken すると後続のトークンを取得しにいく。 115: // 116: // サンプル: 117: // var tokenizer = new AETokenizer(1234 + 90); 118: // var token = null; 119: // while ((token = tokenizer.nextToken()) != null) { 120: // if (token.isEOT()) { 121: // //終了処理 122: // break; 123: // } 124: // if (token.isNUM()) { 125: // token.acceptToken(); 126: // //数字トークンの処理 127: // } 128: // // 129: // } 130: 131: //function AETokenizer_getc() { 132: // if (this.pos < this.text.length) { 133: // return this.text.charAt(this.pos++); 134: // } 135: // 136: // return ""; 137: //} 138: 139: function AETokenizer_getcc() { 140: if (this.pos < this.text.length) { 141: return this.text.charCodeAt(this.pos++); 142: } 143: 144: return -1; 145: } 146: 147: function AETokenizer_ungetc() { 148: if (this.pos > 0) { 149: this.pos--; 150: } 151: } 152: 153: function AETokenizer_isOP1(c) { 154: return (c == AETokenizer.cPlus || c == AETokenizer.cMinus); 155: } 156: 157: function AETokenizer_isOP2(c) { 158: return (c == AETokenizer.cMul || c == AETokenizer.cDiv); 159: } 160: 161: function AETokenizer_isNUM(c) { 162: return (c >= AETokenizer.cZero && c <= AETokenizer.cNine); 163: } 164: 165: function AETokenizer_isSpace(c) { 166: return (c == AETokenizer.cTAB || c == AETokenizer.cSP || c == AETokenizer.cCR || c == AETokenizer.cLF); 167: } 168: 169: function AETokenizer_skipSpace() { 170: var x= -1; 171: while ((x = this.getcc()) > 0) { 172: if (! AETokenizer.isSpace(x)) { 173: this.ungetc(); 174: break; 175: } 176: } 177: } 178: 179: function AETokenizer_readNUM (n) { 180: var s = String.fromCharCode(n); 181: var x = -1; 182: while ((x = this.getcc()) > 0) { 183: if (AETokenizer.isNUM(x)) { 184: s += String.fromCharCode(x); 185: } else { 186: this.ungetc(); 187: break; 188: } 189: } 190: 191: kdebug('tokenizer.readnum', AEToken.NUM); 192: return new AEToken(AEToken.NUM, s); 193: } 194: 195: function AETokenizer_nextToken() { 196: 197: if (this.currentToken != null) { 198: kdebug('tokenizer.nexttoken', this.currentToken.toString()); 199: return this.currentToken; 200: } 201: 202: this.skipSpace(); 203: var x = this.getcc(); 204: if (x < 0) { 205: kdebug('tokenizer.nexttoken', 'EOT'); 206: return new AEToken (AEToken.EOT, ""); 207: } 208: 209: if (AETokenizer.isNUM(x)) { 210: kdebug('tokenizer.nexttoken', 'num starts with' + String.fromCharCode(x)); 211: return (this.currentToken=this.readNUM(x)); 212: } 213: 214: if (AETokenizer.isOP1(x)) { 215: kdebug('tokenizer.nexttoken', 'op2('+String.fromCharCode(x)+')'); 216: return (this.currentToken=new AEToken(AEToken.OP1, x)); 217: } 218: 219: if (AETokenizer.isOP2(x)) { 220: kdebug('tokenizer.nexttoken', 'op2('+String.fromCharCode(x)+')'); 221: return (this.currentToken=new AEToken(AEToken.OP2, x)); 222: } 223: 224: if (x == AETokenizer.cLP) { 225: kdebug('tokenizer.nexttoken', 'LP'); 226: return (this.currentToken=new AEToken(AEToken.LP, x)); 227: } 228: 229: if (x == AETokenizer.cRP) { 230: kdebug('tokenizer.nexttoken', 'RP'); 231: return (this.currentToken=new AEToken(AEToken.RP, x)); 232: } 233: 234: kdebug('tokenizer.nexttoken', 'null'); 235: this.currentToken=null; 236: return null; 237: 238: } 239: 240: function AETokenizer_acceptToken () { 241: kdebug('tokenizer.accepttoken'); 242: this.currentToken=null; 243: } 244: 245: function AETokenizer (text) { 246: 247: this.text = text; 248: this.pos = 0; 249: 250: this.getcc = AETokenizer_getcc; 251: // this.getc = AETokenizer_getc; 252: this.ungetc = AETokenizer_ungetc; 253: 254: this.currentToken = null; 255: 256: this.skipSpace = AETokenizer_skipSpace; 257: this.readNUM = AETokenizer_readNUM; 258: 259: this.nextToken = AETokenizer_nextToken; 260: this.acceptToken = AETokenizer_acceptToken; 261: 262: } 263: 264: AETokenizer.isNUM = AETokenizer_isNUM; 265: AETokenizer.isSpace = AETokenizer_isSpace; 266: AETokenizer.isOP1 = AETokenizer_isOP1; 267: AETokenizer.isOP2 = AETokenizer_isOP2; 268: 269: var ctable = "09+-*/() \r\n"; 270: 271: AETokenizer.cZero = ctable.charCodeAt(0); 272: AETokenizer.cNine = ctable.charCodeAt(1); 273: AETokenizer.cPlus = ctable.charCodeAt(2); 274: AETokenizer.cMinus = ctable.charCodeAt(3); 275: AETokenizer.cMul = ctable.charCodeAt(4); 276: AETokenizer.cDiv = ctable.charCodeAt(5); 277: AETokenizer.cLP = ctable.charCodeAt(6); 278: AETokenizer.cRP = ctable.charCodeAt(7); 279: AETokenizer.cTAB = ctable.charCodeAt(8); 280: AETokenizer.cSP = ctable.charCodeAt(9); 281: AETokenizer.cCR = ctable.charCodeAt(10); 282: AETokenizer.cLF = ctable.charCodeAt(11); 283: 284: 285: // Node クラス 286: // クラスメソッド 287: // Node genNUM(String num) 288: // Node genAPP(int op, Node v1, Node v2) 289: // インスタンスプロパティ 290: // int op (演算子の文字コード) 291: // Node v1 (第一オペランドのノード) 292: // Node v2 (第二オペランドのノード。op が単項演算子の場合は null) 293: // インスタンスメソッド 294: // boolean isNUM () 295: // boolean isAPP () 296: 297: function Node_isNUM () { 298: return (this.type == Node.NUM); 299: } 300: 301: function Node_isAPP () { 302: return (this.type == Node.APP); 303: } 304: 305: function Node_toString () { 306: if (this.isNUM()) { 307: return this.v1; 308: } 309: if (this.isAPP()) { 310: if (this.v2 == null) { 311: // 単項演算子のとき 312: return String.fromCharCode(this.op) + "(" + this.v1.toString() + ")"; 313: } 314: return "(" + this.v1.toString() + String.fromCharCode(this.op) + this.v2.toString() + ")"; 315: } 316: return "unknown"; 317: } 318: 319: function Node_genAPP (op, v1, v2) { 320: return new Node(Node.APP, op, v1, v2); 321: } 322: 323: function Node_genNUM (v) { 324: return new Node(Node.NUM, "", v, null); 325: } 326: 327: function Node (type, op, v1, v2) { 328: this.type = type; 329: this.op = op; 330: this.v1 = v1; 331: this.v2 = v2; 332: this.isNUM = Node_isNUM; 333: this.isAPP = Node_isAPP; 334: 335: this.toString = Node_toString; 336: } 337: 338: Node.NUM = 0; 339: Node.APP = 1; 340: Node.genNUM = Node_genNUM; 341: Node.genAPP = Node_genAPP; 342: 343: 344: // AEParser クラス 345: // コンストラクタ 346: // AEParser (String text) 347: // インスタンスメソッド 348: // Node parse () 349: // 戻り値 null --- パース失敗 350: 351: function AEParser_readExp3 () { 352: kdebug('parser.readexp3', 'start'); 353: var x = this.tn.nextToken(); 354: if (x == null) { 355: kdebug('parser.readexp3', 'null1'); 356: return null; 357: } 358: 359: var r = null; 360: 361: if (x.isLP()) { 362: this.tn.acceptToken(); 363: kdebug('parser.readexp3', 'LP found'); 364: if ((r = this.readExp1()) != null) { 365: if ((x = this.tn.nextToken()) != null) { 366: if (x.isRP()) { 367: this.tn.acceptToken(); 368: kdebug('parser.readexp3', 'RP found.'); 369: kdebug('parser.readexp3', r.toString()); 370: return r; 371: } 372: } 373: } 374: kdebug('parser.readexp3', 'null2'); 375: return null; 376: } 377: if (x.isOP1()) { 378: this.tn.acceptToken(); 379: if ((r = this.readExp2()) != null) { 380: kdebug('parser.readexp3', 'op1+exp2'); 381: return Node.genAPP(x.value, r, null); 382: } 383: kdebug('parser.readexp3', 'null3'); 384: return null; 385: } 386: 387: if (x.isNUM()) { 388: this.tn.acceptToken(); 389: kdebug('parser.readexp3', 'num:'+x.value); 390: return Node.genNUM(x.value); 391: } 392: 393: kdebug('parser.readexp3', 'null4'); 394: 395: return null; 396: } 397: function AEParser_readExp2() { 398: kdebug('parser.readexp2', 'start'); 399: var v1 = this.readExp3(); 400: 401: if (v1 == null) { 402: kdebug('parser.readexp2', 'null1'); 403: return null; 404: } 405: 406: var op = this.tn.nextToken(); 407: 408: var v2 = null; 409: 410: while (op != null && op.isOP2()) { 411: this.tn.acceptToken(); 412: if ((v2 = this.readExp3()) != null) { 413: v1 = Node.genAPP(op.value, v1, v2); 414: } else { 415: kdebug('parser.readexp2', 'null2'); 416: return null; 417: } 418: op = this.tn.nextToken(); 419: } 420: 421: if (op == null) { 422: kdebug('parser.readexp2', 'null3'); 423: return null; 424: } 425: 426: kdebug('parser.readexp2', 'end'); 427: 428: return v1; 429: } 430: 431: function AEParser_readExp1() { 432: kdebug('parser.readexp1', 'start'); 433: var v1 = this.readExp2(); 434: 435: if (v1 == null) { 436: kdebug('parser.readexp1', 'null1'); 437: return null; 438: } 439: 440: var op = this.tn.nextToken(); 441: 442: var v2 = null; 443: 444: while (op != null && op.isOP1()) { 445: this.tn.acceptToken(); 446: if ((v2 = this.readExp2()) != null) { 447: v1 = Node.genAPP(op.value, v1, v2); 448: } else { 449: kdebug('parser.readexp1', 'null2'); 450: return null; 451: } 452: op = this.tn.nextToken(); 453: } 454: 455: if (op == null) { 456: kdebug('parser.readexp1', 'null3'); 457: return null; 458: } 459: 460: kdebug('parser.readexp1', 'end'); 461: 462: return v1; 463: 464: } 465: 466: function AEParser_parse () { 467: var n = this.readExp1(); 468: if (n == null) { 469: return null; 470: } 471: var token = this.tn.nextToken(); 472: if (token != null && token.isEOT()) { 473: return n; 474: } 475: return null; 476: } 477: 478: function AEParser (text) { 479: this.tn = new AETokenizer(text); 480: 481: this.parse = AEParser_parse; 482: this.readExp1 = AEParser_readExp1; 483: this.readExp2 = AEParser_readExp2; 484: this.readExp3 = AEParser_readExp3; 485: } 486: 487: // End of arith_exp_parser.js
●動作確認1
WSH で動作確認する WSF スクリプト。sample.wsf は arith_exp_parser.js、disptree.js と同じディレクトリに保存し、ダブルクリックで実行。表示される入力ダイアログに数式を入力して「OK」ボタンを押すと、数式の括弧付き表現とツリー表現を表示する。 数式の入力には VBScript の InputBox 関数を使用。JScript に文字入力のプリミティブがないので仕方がないが、WSF スクリプトでは複数のスクリプト言語を組み合わせることが可能。
001: <?xml version="1.0" encoding="Shift_JIS" standalone="yes" ?> 002: <package> 003: <job id="aexp"> 004: <runtime> 005: <description>Sample of arith_exp_parser.js</description> 006: </runtime> 007: 008: <?job error="true" debug="true" ?> 009: <script language="javascript" src="arith_exp_parser.js" /> 010: <script language="javascript" src="disptree.js"/> 011: <script language="VBScript"> 012: <![CDATA[ 013: Function inputExp 014: inputExp = InputBox("計算式を入力してください。", "計算式", "1234*2345+34/456-56") 015: End Function 016: ]]> 017: </script> 018: <script language="JScript"> 019: <![CDATA[ 020: try { 021: var p = new AEParser(inputExp()); 022: var n = null; 023: WScript.Echo((n=p.parse())?n.toString():"syntax error."); 024: WScript.Echo(disptree(0, "", n, "\n")); 025: } catch (e) { 026: WScript.Echo(Number(e.number & 0xffff).toString() + ": " + e.description); 027: } finally { 028: WScript.Quit(); 029: } 030: ]]> 031: </script> 032: </job> 033: </package>
●動作確認2
ブラウザで動作確認。数式を入力して「parse it!」ボタンのクリックで、ツリー表現を表示する。