实现一个计算器
产生原因:
(1)一直以来,我都想写一门语言,但无从下手。
(2)我找到了很多编译原理的教程,但始终觉得内容晦涩,理解不了,所以先尝试写一个简单的,比如:计算器。
(3)网上有很多关于计算器的实现,但大多需要有编译原理的基础,对于我这种小白实在难以理解。
(4)我决定采用暴力模拟的方式,需要用正则表达式,但我不想自己实现,所以用js。
最终要实现什么效果
计算器接受一串字符,处理后返回结果。
我们来看一下要做什么:
首先需要知道有哪些“元素”,比如“12+34×56"的元素有整数12,加号,整数34,乘号,整数56,这个过程称为词法分析。
然后根据符号的优先级进行组合,其过程相当于加括号,12+(34*56),这个过程称为语法分析。
借用正则表达式,可以简单暴力的实现词法分析。
什么是正则表达式
正则表达式的概念,和编译原理一样,都要费好大功夫来理解,当初也是各种查资料。
尽量简单的讲一讲吧。
定义:正则表达式是一种生成器,可以生成大量相同模式的字符串。
字符串的概念大家都懂,来看一下正则表达式是怎么生成字符串的。
例子:正则表达式 ab* 可以生成'a' , 'ab' , 'abb' , 'abbb' ...
正则表达式有三种规则(并,或,闭包),其他规则都是由这三个规则组合而成。
并,直接连在一起,表示相连,例如:abc 生成’abc'
或,以符号|分隔,表示或,例如:a|b生成'a','b'
闭包,加符号*,表示重复任意次,例如:a* 生成 '','a', 'aa', 'aaa'...
一些常用的规则:
[],例如:[abcd]等价于a|b|c|d
+,例如:a+等价于aa*
?,例如:a?等价于 a|空
\d,等价于[0123456789],还可以用[0-9]
\w,等价于[A-Za-z0-9]
\W,非\w
\b,表示\w与\W的交界处
词法分析
计算器用到的正则表达式:
+:\+
-:-
*:\*
/:/
(:\(
):\)
number:\b\d+\b
注意:规则中有的字符要转义,如:+
不断用正则来匹配输入的字符串,就可以。
使用js来实现:
1 function get_words(buf){ 2 patterns = [ 3 ['(', /\(/], 4 [')', /\)/], 5 ['+', /\+/], 6 ['-', /-/], 7 ['*', /\*/], 8 ['/', /\//], 9 ['number', /\b\d+(\.\d+)?\b/] 10 ]; 11 12 words = []; 13 flag = true; 14 while (buf && flag) { 15 flag = false; 16 for (p in patterns) { 17 buf = buf.trimLeft(); 18 ex = patterns[p][1].exec(buf); 19 if (ex && ex['index'] == 0) { 20 str = ex[0]; 21 flag = true; 22 buf = buf.slice(str.length,Infinity); 23 words.push([patterns[p][0], parseFloat(str)]); 24 break; 25 } 26 } 27 } 28 return words; 29 }
对于'12+34 * (78-56)',会得到:
number,12 +,NaN number,34 *,NaN (,NaN number,78 -,NaN number,56 ),NaN
至此,词法分析完成。
语法分析
我们采用类似于正则的方式来描述语法分析的过程,可称其为文法。
分析一波。
括号优先级最高,所以遇见括号就要计算,文法为:
<factor> => ( '(' <expr> ')' ) | 'number'
引号的称终结符,尖括号的称非终结符。
非终结符表示可以继续推导,终结符表示推导终点。
其中<expr>表示整个算式
乘除优先级比加法高,所以遇见乘除就要计算,文法为:
<term> => <factor> ( ( '*' <factor> ) | ( '/' <factor> ) ) *
然后是加减:
<expr> => <term> ( ( '+' <term> ) | ( '-' <term> ) ) *
这些都可以用正则来理解。
其中每个非终结符都做成一个函数,翻译过来就成。
1 function parse(words) { 2 // <expr> => <term> (('+' <term>) | ('-' <term>))* 3 // <term> => <factor> (('*' <factor>) | ('/' <factor>))* 4 // <factor> => ('(' <expr> ')') | 'number' 5 p = 0; 6 7 function type() { 8 if (p >= words.length) return null; 9 return words[p][0]; 10 } 11 function match(sym) { 12 if (words[p][0] == sym) { 13 return words[p++][1]; 14 } 15 console.log('\nerror\n'); 16 } 17 function expr() { 18 value = term(); 19 while (type() == '+' || type() == '-') { 20 if (type() == '+') { 21 match('+'); 22 value += term(); 23 } else { 24 match('-'); 25 value -= term(); 26 } 27 } 28 return value; 29 } 30 function term() { 31 value = factor(); 32 while (type() == '*' || type() == '/') { 33 if (type() == '*') { 34 match('*'); 35 value *= factor(); 36 } else { 37 match('/'); 38 value /= factor(); 39 } 40 } 41 return value; 42 } 43 function factor() { 44 if (type() == '(') { 45 match('('); 46 value = expr(); 47 match(')'); 48 } else if (type() == 'number') { 49 value = match('number'); 50 } 51 return value; 52 } 53 54 return expr(); 55 }
写完了,哈哈。
总结
用node.js可以简单的跑起来:
折起来吧,效果在前面。