百分比计算器(手机计算器为什么会出现10%+10%=011这样明显错误的算式?答案来)
2024-05-10 09:45 来源:爱美欣 浏览量:次
这是一个历史遗留问题,属于语法糖,叫做百分计算器。
按人类语义的理解,你去买东西,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
devblogs.microsoft.com
虽然早期百分运算的用法很简单,但是如今的手机计算器可以输入连续的表达式,最后输出结果(部分手机计算器还有即时回显功能)。表达式计算满足优先级。但是计算器中的百分号非常特殊,它的功能实际与前后的环境与算法的选择有关。
比如:
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
github.com
double s[] = context.stackRe; int percentPC = -2; for (int pc = 0; pc < codeLen; ++pc) { final int opcode = code[pc]; switch (opcode) { case VM.CONST: s[++p] = constsRe[constp++]; break; case VM.ADD: { final double a = s[--p]; double res = 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; case VM.SUB: { final double a = s[--p]; double res = 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; } case VM.PERCENT: s[p] = s[p] * .01; percentPC = pc; break; } return p;
我已去除和百分运算无关的部分。
下面对该代码运算过程举个例子:
表达式: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]就代表结果,完结。
可以明显看出,加减法中多了一步判断:
double res = a + (percentPC == pc-1 ? s[p] * s[p+1] : s[p+1]);
本质就是查看后缀表达式+-之前的符号是否为%来执行该+-的操作。
如果不需要该特性,只需将这一句改为:
res = a + s[p+1];
另外有网友提出括号的问题,部分计算器的后缀表达式生成时,遇到左括号“(”会将其作为一个标记插入队列。于是,a+(b%)后缀表达式会变成 a b % mark +,加号之前的符号不再是%,不再执行特殊百分比加法。也有计算器加了括号也没有用,这也很好推断,该计算器在生成后缀表达式时没有对括号作插入标记。
计算器的处理过程就是这么简单粗暴,也不涉及什么高深的算法。对于百分运算的特殊处理也只需多一个指针就能做到。所以你能想到了,要适应国内的习惯,只需要加一个地区判断替换语句就可以了。
个人建议在使用手机计算器时,在复杂连续表达式中避免使用+10%这种写法,因为不同的厂商算法不同,计算逻辑也不同。尽量转化为小数或者在百分数前加基数,比如+1x10%。
- 2024-05-27[科技知识]锂电池和铅酸电池(铅酸电池和锂电池电动车,到底有啥区别,选哪个更划算?)
- 2024-05-26[科技知识]产业生态(新能源汽车完善产业生态链“出海” 凭借技术优势加快“走出去”)
- 2024-05-26[科技知识]单车事故(安全事故频发,监管全面收紧,电动自行车加速驶入正轨)
- 2024-05-26[科技知识]新能源汽车概念股(新能源汽车迎重磅利好这4只概念股站上风口,后市要涨飞?名单)
- 2024-05-26[科技知识]广汽suv(一部接近完美的国产纯电动SUV,试驾广汽新能源Aion V)
- 2024-05-26[科技知识]信达国际(信达国际融资与深圳前海嘉德能源携手启航,共谱香港上市新篇章)
- 2024-05-26[科技知识]忙得不亦乐乎(港媒:广交会,中国新能源出口商忙得不亦乐乎)
- 2024-05-26[科技知识]新集能源股票(煤炭行业板块下挫,新集能源(601918CN)跌754%)