首页 > 技术文章 > 实现一个计算器

chenliyang 2017-03-14 14:08 原文

产生原因:

(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可以简单的跑起来:

折起来吧,效果在前面。

推荐阅读