实现一个简单的计算器

2 篇文章 0 订阅
1 篇文章 0 订阅

小标题:一个计算器引发的血案!

故事:

        几个星期前,我和我的小伙伴一起去华联超市买东西。它想去买菜,我想去买零食,我们说好了不买其他东西。不幸的是,我们高高兴兴的从超市的一楼逛到三楼的时候,小车已经不能装下更多的东西了。我们互喷了一句,很不高兴的付了账,很高兴的把东西搬了回去。

        当回去分赃的时候,我发现一个问题:我的小伙伴刷卡付账,这意味着我必须把钱给他。尽管我非常不想给它钱,但是还是要给它钱。我拿出账单的时候,惊呆了:账单密密麻麻足足有两米!这是不是意味着,等我算清楚的时候,小伙伴已经老死了。当然,聪明的我决不会用手指算这种垃圾。本人经过慎重研究,考虑到计算的重复性特征,决定采用21世纪西方最先进的计算机技术,完成小伙伴死之前还它钱的伟大壮举!

       于是就又有了这个看起来不牛叉,实际上也不牛叉的计算器!We call it Calc-V1

 

       用法:输入一串加减乘除的四则运算表达式,得到这个表达式的结果。

       输入:合法的四则运算表达式,包括:+ - * / () 和取反;识别整数、十六进制数、小数(不包括科学计数法格式)

       输出:计算结果(小数或者整数)

       要求:正确计算+ - * / () 取反,正确识别整数、小数(不包括科学计数法,因为华联超市的账单上没有使用科学计数法)。不识别不正确的表达式。

       分析:四则运算的计算机实现一点都不难,因为计算机本身就支持四则运算中的所有操作(+-*/),这里的主要问题是如何正确识别运算优先级,从而正确计算结果。

 

       优先级无非是:先乘除后加减,先括号后外边。

 

       如果用推导式表达的话(参考编译原理的书籍),如下:

Expression >> expression + add-item

Expression >> expression - add-item

Expression >> add-item

Add-item >> add-item * multi-item

Add-item >> add-item / multi-item

Add-tiem >> multi-item

Multi-item >> number

Multi-item >> (expression)

Multi-item >> -(expression)

Multi-item >> +(expression)

       这个文法(推导系统)是左递归的,消除左递归后,可以被自上而下的LL(1)分析所识别(参考编译原理)。如下(null代表空)

Expression >> add-item add-expression

Add-expression >> + add-item add-expression

Add-expression >> - add-item add-expression

Add-expression >> null

Add-item >> multi-item multi-expression

Multi-expression >> * multi-item multi-expression

Multi-expression >> / multi-item multi-expression

Multi-expression >> null

Multi-item >> number

Multi-item >> (expression)

Multi-item >> -(expression)

Multi-item >> +(expression)

       可以构造LL(1)分析表来识别合法的四则运算表达式,然后使用语法制导翻译,解决四则运算优先级问题。(注:虽然我没有成功,语义分析做不下去了,但是这个方法肯定是可以的,只是语义计算可能有点复杂。)

 

       作为一个正常的人,遇到这样的语义问题,肯定会放弃,就和我做的一样。放弃后,我唯一觉得不开心的是:不就是计算加减乘数吗,上帝有必要把它搞这么复杂吗?

       人类一思考,上帝就发笑。就在我觉得这事情不可理喻的时候,脑子里突然一道灵光:我找到了,就像牛顿头上的烂苹果。所谓伟大的。。。

       其实,事情是这样的,大概在很多个日子之前,有个什么物质(文章、或者中国人)提过到如何用栈实现有优先级的四则运算。大致的思路是:维护一个操作数的栈和运算符号的栈,如果当前读取到的运算符号比较性感,就会吸引该运算符前面的操作数,使它不会投入到前一个运算符的怀抱;相反,如果当前运算符看起来非常屌丝,就会吓跑它前面的操作数,使该操作数和前一个运算符结合。

       例如:1+2+3Calc-V1会一直读取操作数和运算符,直到读取到第二加号时,奇妙的事情就发生了,这个加号和第一个加号是同一个优先级的,也就是说他们长得一样帅,于是第二个加号先生让我们的2小姐很不高兴:不仅长得丑,约会还迟到,屌丝!于是2就和第一个加号牵手,和1完成了加法运算,计算结果放回原位置,变成3+3

       再比如:1+2*3Calc-V1会一直读取操作数和运算符,直到读取到*时,奇妙的事情就发生了。*可不是臭加号,它拥有钻石的外表,高富帅的风度。2小姐当然不是傻子,她甩开哪怕是先到的加号先生的手,毅然决然的奔向*哥哥!当然,3还没有到来,不过天已经注定了他和2小姐的爱情!

       所以四则运算就是谁帅谁性感的问题,真是俗!

 

       下面是代码,为了方便(不是为了‘方便’,你们太邪恶了),采用perl语言,脚本语言都很好理解,没学过也照样看得懂。如果想改写成其他语言,照葫芦画瓢就好了。

       说明:在完成屌丝和高富帅的战争之前,有一个简单的词法分析,也就是程序把一个表达式识别成一个个的单词,而不是一个个字符。比如:123+234,应该是’123’和’+’和’234’,而不是123等等。解释注释都是掩饰,上代码!

#!/usr/bin/perl -w
### tools func ###
$local_debug = 1;
sub Info($){ print "[INFO ]@_\n"; }
sub Die($) { die "@_\n"; }
sub Err($) { print "[ERROR]@_\n"; }
sub De($)  { $local_debug == 0 or print "[DEBUG]@_\n"; }
### end        ###

### global variables
@buf = ();#the string char buffer 
@back = ();# the unused token 
$status = "value";# the status of the operation
@stack = ();# the stack to keep operators and numbers
$base = 0;# the base priority of new tokens
$offset = 3;# the priority of an () will improved
$unary_op = "";# the unary + or -, to support +12, -4.5 
### global variables

### main start
if(@ARGV > 0){#read from cmd line
    $string = join("",@ARGV);
    $res = &calc($string);
    print "=$res\n";
}else{
    while(<STDIN>){#read from pipeline or cmd line
        if(/^\s*$/ or /^#/ ){ next; }
        $res = &calc($_);
        print "$res=$_\n";
    }   
}
exit;
### main end 
                                                                                                                                                                                 
### define
# operator: + - * / +( -( ( )  #
# priority: 0 0 1 1 2  2  2 2  -1
# the '#' is the end of token string
# number: int[12 13 15] float:[0.1 1.1 1. ] hex[0x00 0X0F]
# token attribute: + - op_plus, * / op_multi, ( +( -( op_left, ) op_right, int num_int, float num_float, hex num_hex
### end define

sub calc{
    if( @_ == 0){ return ;}
    my $string = shift@_;

    #init 
    @buf = split(//, $string);
    $status = "value";# the status transfer table: value -> operator, operator -> value, value -> number -> operator
    @back = ();#buffer the unused token 

    @stack = ();#all operators and numbers are inserted in this stack
    @priority = ();#all token's priority are inserted in this stack 
    $base = 0;
    $unary_op = "";
    #run
    while(1){#status transfer machine 
        De "stats: $status";
        my @token = &get_token();
        if( $token[1] eq "ERROR"){ Die "unexpected char:$token[0]"; }
        De "token:@token";
        if($status eq "value"){ &actionValue(@token);}
        elsif($status eq "number"){ &actionNumber(@token); }
        elsif($status eq "operator"){ &actionOperator(@token);}
        elsif($status eq "end"){ last; }
        else{ Die "unexpected status:$status"; }
        De "stack:@stack";
        #De "prior:@priority";
    }
    return $stack[0];
}
sub get_priority{
    my $a = $_[0];
    if($a eq "+" or $a eq "-"){ return 0; }
    elsif($a eq "*" or $a eq "/"){ return 1; }
    elsif($a =~ /\(/ or $a eq ")" ){ return 2; }
    elsif($a eq "#"){ return -1; }
    else{ Die "bad token to ask for priority:$a"; }
}
sub actionValue{
    my($token, $attr) = @_;
    if( $attr =~ /^num/ ){
        $token = $attr eq "num_hex" ? hex($token) : 1*$token;
        push@stack, $token;
        push@priority, 0;#the number's priority is ignored
        $status = "operator";
    } elsif ( $token eq "+" || $token eq "-" ){
        $unary_op = $token;# needs a number to build a value, +12,-2.3 etc.
        $status = "number";
    } elsif ( $token eq "+(" || $token eq "-(" || $token eq "("){
        push@stack, $token;
        push@priority, $base+&get_priority($token);
        $base += $offset;#improve the priority
        $status = "value";#alse needs a value 
    }else{ Die "unexpected token: $token, where needs a value or its prefix:+-"; }
}
sub actionNumber{
    my($token, $attr) = @_;
    if( $attr =~ /^num/ ){
        $token = $attr eq "num_hex" ? hex($token) : 1*$token;
        if($unary_op eq "-"){ $token = -1*$token; }
        elsif($unary_op eq "+"){}
        else{ Die "unexpected unary operator:$unary_op"; }
        push@stack, $token;
        push@priority, 0; # the number's priority is ignored 
        $status = "operator";
    }else{
        Die "unexpected token: $token, where needs a number for its prefix:$unary_op";
    }
}
sub actionOperator{
    my($token, $attr) = @_;
    if ( $token eq "+" || $token eq "-" || $token eq "*" || $token eq "/"){
        if(&actionCalc($token) eq "no_action"){# no action means to shift in the token
            push@stack, $token;
            push@priority, $base+&get_priority($token);
            $status = "value";
        }else{ #the action will change the status in the @stack, use the same token try again 
            @back = ($token, $attr);
            $status = "operator";
        }
    } elsif ( $token eq ")" ){
        if(&actionCalc($token) ne "matched"){
            @back = ($token, $attr);
        }else{
            $base -= $offset;
            $base >=0 or Die "two many ')'";
        }
        $status = "operator";
    } elsif( $token eq "#"){#means the end of the string 
        $base == 0 or Die "unexpected end of calc string, needs more right parentheses";
        if(&actionCalc($token) eq "no_action"){
            @stack == 1 or Die "bad end of stack:@stack";
            $status = "end";
        }else{
            @back = ($token, $attr);
            $status = "operator";
        }
    }else{ Die "unexpected token: $token, where needs an op"; }
}
sub actionCalc{
    my $next_operator = $_[0];
    my $next_priority = $base + &get_priority($next_operator);
    $next_priority -= $next_operator eq ")" ? $offset : 0;
    if(@stack < 3){ return "no_action"; }#at least has this pattern: number operator number 
    my $current_priority = $priority[-2];# get the last operator
    if($current_priority >= $next_priority){
        if(&is_binary_op($stack[-2])){
            my $value = &oneOPtwo($stack[-3], $stack[-2], $stack[-1]);
            pop@stack;pop@stack;pop@stack;push@stack, $value;
            pop@priority;pop@priority;pop@priority;push@priority,0;
            return "calc";
        }elsif( &is_left_parentheses($stack[-2])){
            my $value = pop@stack;
            my $op = pop@stack;
            $value = $op eq "-(" ? -1*$value : $value;
            push@stack, $value;
            pop@priority;pop@priority;push@priority,0;
            return "matched";
        }else{ Die "bad stack content at:$stack[-2]"; }
    }else{ return "no_action"; }
}
sub is_binary_op{
    my $op = shift@_;
    return $op eq "+" || $op eq "-" || $op eq "*" || $op eq "/";
}
sub is_left_parentheses{
    my $op = $_[0];
    return $op eq "+(" || $op eq "-(" || $op eq "(";
}
sub oneOPtwo{
    my($one, $op, $two) = @_;
    De "calc : $one $op $two";
    my $res = 0;
    if( $op eq "+" ){ $res = $one + $two; }
    elsif( $op eq "-" ){ $res = $one - $two; }
    elsif( $op eq "*" ){ $res = $one * $two; }
    elsif( $op eq "/" ){ $res = $one / $two; }
    else{ Die "bad op : $op"; }
    return $res;
}
### end 

### token func
sub get_token{
    my @token = ("#", "#");
    if( @back != 0 ){ 
        @token = @back;
        @back = ();
        return @token;
    }
    #ignore whitespace 
    while( @buf > 0 && &is_whitespace($buf[0]) ){ shift@buf; }
    if( @buf > 0){
        my $ch = shift@buf;
        if( $ch eq "+" || $ch eq "-" ){ 
            if( @buf > 0 && $buf[0] eq "(" && $status eq "value"){ 
                shift@buf;                                                                                                                                                        
                @token = ("$ch(", "op_left");
            }else { @token = ($ch, "op_plus"); }
        }elsif( $ch eq "*" ){ @token = qw(* op_multi); }
        elsif( $ch eq "/" ){ @token = qw(/ op_multi); }
        elsif( $ch eq "(" ){ @token = qw%( op_left%; }
        elsif( $ch eq ")" ){ @token = qw%) op_right%; }
        elsif( &is_digit($ch) ){ @token = &get_digit($ch); }
        else { Err "unexpected char: '$ch'"; @token = ($ch, "ERROR");}
    }
    return @token;
}
sub get_digit{
    my @num = ();
    push@num, $_[0];
    # handle hex-format: 0x00
    if( $num[0]  eq "0" && @buf > 1 && ($buf[0] eq "x" || $buf[0] eq "X") && &is_hexdigit($buf[1]) ){
        shift@buf;
        @num = qw(0 x);
        while( @buf > 0 && &is_hexdigit($buf[0]) ){ 
            push@num, $buf[0];
            shift@buf;
        }
        my $token = join "", @num;
        return ($token, "num_hex");
    }
    while( @buf > 0 && &is_digit($buf[0]) ){
        push@num, $buf[0];
        shift@buf;
    }
    # if is not .
    if( @buf == 0 || @buf > 0 && $buf[0] ne "." ){
        my $token = join "", @num;
        return ($token, "num_int");
    }
    # handle .
    push@num, $buf[0];
    shift@buf;
    while( @buf > 0 && &is_digit($buf[0]) ){
        push@num, $buf[0];
        shift@buf;
    }
    my $token = join "", @num;
    return ($token, "num_float");
}
sub is_whitespace{
    my $ch = $_[0];
    return $ch eq " " || $ch eq "\t" || $ch eq "\n" || $ch eq "\f" || $ch eq "\r" ;
}
sub is_digit{
    my $ch = $_[0];
    return $ch eq "0" || $ch eq "1" || $ch eq "2" || $ch eq "3" || $ch eq "4" 
        || $ch eq "5" || $ch eq "6" || $ch eq "7" || $ch eq "8" || $ch eq "9";
}
sub is_hexdigit{
    my $ch = $_[0];
    return $ch eq "0" || $ch eq "1" || $ch eq "2" || $ch eq "3" || $ch eq "4"
        || $ch eq "5" || $ch eq "6" || $ch eq "7" || $ch eq "8" || $ch eq "9"
        || $ch eq "a" || $ch eq "b" || $ch eq "c" || $ch eq "d" || $ch eq "e" || $ch eq "f"
        || $ch eq "A" || $ch eq "B" || $ch eq "C" || $ch eq "D" || $ch eq "E" || $ch eq "F";
}

 

       有了这个神器,妈妈再也不用担心我的学习了。开玩笑。有了这个Calc-V1,以后再也不用担心计算中途的时候,该死的鼠标点错了啊(是不是很开心)。

 

       故事结局:当我找出编译原理的书,倒腾出推导式,消除左递归,完成LL分析发现困难重重,又被烂苹果砸到脑袋,采用perl,在linux下完成编码和测试,最后计算出正确答案的时候,悄悄的,北京的房价又涨了几千,人民币又贬值了几块,太阳东升西落了几次。

       微风吹开我的长发,我拿着写着结果的纸片,轻盈的走向我的小伙伴,它,它,它,居然不理我了!不理我了!居然不理我了!!!(旁观者眼中:主角得不到小伙伴的认可,吐血身亡。)

 

       画外音:天才都短命。珍爱生命,远离天才。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值