1. XenForo 1.5.14 中文版——支持中文搜索!现已发布!查看详情
  2. Xenforo 爱好者讨论群:215909318 XenForo专区

手机计算器为什么会出现10%+10%=0.11这样明显错误的算式?

本帖由 漂亮的石头2021-04-13 发布。版面名称:知乎日报

  1. 漂亮的石头

    漂亮的石头 版主 管理成员

    注册:
    2012-02-10
    帖子:
    486,293
    赞:
    46
    [​IMG] 遇见,C++开发 阅读原文

    这是一个历史遗留问题,属于语法糖,叫做百分计算器。

    按人类语义的理解,你去买东西,100 元钱减去 10%,那就是 90 元。早期的计算器就可以直接这样写 100-10%。再比如,一只股票股价 10 元,增长了 50%,可以直接写 10+50%。这么设计更深层次的原因可能与早期计算器的按键数量有限,以及单步运算的性质有关。具体有答主已经作了回答。

    手机计算器保留了这种特性

    10%+10% 就是 0.11。

    至于部分国内计算器结果是 0.2,是因为国内手机厂商自己做了修改,符合中国人打几折的说法。上述的 100-10% 其实是外国人的逻辑,在国外商品打 9 折叫 10% off。

    魅族的工程师已经在微博说明他们在国内使用了 0.2 的方案,在国外使用 0.11 的方案。

    9.7 更新:经调查各厂商的百分计算逻辑存在标准不统一的问题,复杂算式中对百分号的处理存在较大差异,具体差异已经合并写入识别条件中。

    下面有早期计算器百分键功能的具体说明。

    How does the calculator percent key work? | The Old New Thing

    虽然早期百分运算的用法很简单,但是如今的手机计算器可以输入连续的表达式,最后输出结果(部分手机计算器还有即时回显功能)。表达式计算满足优先级。但是计算器中的百分号非常特殊,它的功能实际与前后的环境与算法的选择有关。

    比如:

    5+5*10+10%+5=?

    5+(10%)=?

    5+10%*10=?

    (如果你坚信你自己的想法,你可以用你的理论去算这些式子,然后用手机计算器验证。)

    要知道这些结果,我们需要了解百分运算的识别条件。

    百分计算识别条件

    exp1 [+-] exp2 % [+-*/] exp3

    • exp1 可以是表达式也可以是单独的数字,比如 5,5+5,5+5x5,(5+5)。
    • exp1 的值会被优先计算,比如 5+5-10%=(5+5)x(1-10%)=9
    • exp2 可以是单独的数字或者带括号的表达式,比如 5,(5+5)。
    • 如 exp2 与 exp3 之间为 [ * / ],不同厂商有不同的处理方式。第一种会将 exp2 % [* /] exp3 作为整体计算成数值,比如 5+10%*10=6。第二种会将 exp2 % [* /] exp3 作为增长率,比如 5+10%*10=5+100%=10。
    • 有关在 exp2%前后加括号的问题,即 exp1[+-](exp2%)这种情况,不同计算器会有不同的处理方式,括号不一定会影响结果,比如 10+(10%)可能等于 11,也可能等于 10.1。这涉及代码处理,已在最后更新。
    • 实际含义:在满足识别条件的情况下,对之前的累计结果增长或减少一个百分比。

    要知道计算器如此工作的原因,我们可以直接从源码入手。

    源码分析:

    我找了一份 Github 上计算器的源码。

    和大多数计算器的处理方法一致,先将原表达式转化为后缀表达式,利用数字栈和操作符栈,配合指针,从左到右扫描一次就可以得出答案。

    hoijui/arity

    doubles[]=context.stackRe;intpercentPC=-2;for(intpc=0;pc<codeLen;++pc){finalintopcode=code[pc];switch(opcode){caseVM.CONST:s[++p]=constsRe[constp++];break;caseVM.ADD:{finaldoublea=s[--p];doubleres=a+(percentPC==pc-1?s[p]*s[p+1]:s[p+1]);if(Math.abs(res)<Math.ulp(a)*1024){// hack for "1.1-1-.1"res=0;}s[p]=resbreak;caseVM.SUB:{finaldoublea=s[--p];doubleres=a-(percentPC==pc-1?s[p]*s[p+1]:s[p+1]);if(Math.abs(res)<Math.ulp(a)*1024){// hack for "1.1-1-.1"res=0;}s[p]=res;break;}caseVM.PERCENT:s[p]=s[p]*.01;percentPC=pc;break;}returnp;

    我已去除和百分运算无关的部分。

    下面对该代码运算过程举个例子:

    表达式:a+b%+c 表示成后缀表达式:ab%+c+ Code 队列:[ a , b, % , + , c , +] 有个 s 栈,开始为空:[] 一共三个指针:p、pc、percentPC, 初始值分别为 -1,-1,-2。 每次遇到常数,p 自增 1,再在 s 中 p 指向的位置放入该常数。 每次遇到 +-,p 会自减 1。 每次遇到%,令 p 指向的内容乘以 0.01,percentPC=pc。 从左向右开始扫描 code,pc 为指针,右移一次 pc 增 1。 首先遇到常数 a,b,放入 s 中:[a,b] ,p 指向 b 继续扫描,遇到%,将 p 指向的内容 *0.01,s 变成:[a , b*0.01];同时,percentPC 指向 code 中的%。 继续扫描,遇到 +,pc 此时指向的位置为 percentPC+1,由三元判断式,a=a+a*b*0.01,p 重新指向 a,s 变为[a+a*b*0.01,b*0.01] 继续扫描,c 替代 b*0.01 继续扫描,遇到 +,此时的 pc 不等于 percentPC+1,s[p]=s[0]=a+a*b*0.01+c 结束扫描,返回指针 p,s[p]就代表结果,完结。

    可以明显看出,加减法中多了一步判断:

    doubleres=a+(percentPC==pc-1?s[p]*s[p+1]:s[p+1]);

    本质就是查看后缀表达式 +- 之前的符号是否为 % 来执行该 +- 的操作。

    如果不需要该特性,只需将这一句改为:

    res=a+s[p+1];

    另外有网友提出括号的问题,部分计算器的后缀表达式生成时,遇到左括号“(”会将其作为一个标记插入队列。于是,a+(b%)后缀表达式会变成 a b % mark +,加号之前的符号不再是%,不再执行特殊百分比加法。也有计算器加了括号也没有用,这也很好推断,该计算器在生成后缀表达式时没有对括号作插入标记。

    计算器的处理过程就是这么简单粗暴,也不涉及什么高深的算法。对于百分运算的特殊处理也只需多一个指针就能做到。所以你能想到了,要适应国内的习惯,只需要加一个地区判断替换语句就可以了。

    个人建议在使用手机计算器时,在复杂连续表达式中避免使用 +10% 这种写法,因为不同的厂商算法不同,计算逻辑也不同。尽量转化为小数或者在百分数前加基数,比如 +1x10%。

    阅读原文
     
正在加载...