(如果您在本文中发现有错别字或明显笔误,请留言指出,如确实有错我将尽快更正,谢谢。)
说起计算器,从小学起就开始用了吧,不过要说研究应该是初中时。简单地说就是学会了四个M键的用法,更进一步地说发现了许多有趣的组合键。对于我使用的那个计算器来说,最早发现的是同 时按下×、÷、-就会出现9,然后又发现同时按住×、÷再按ON就能关机,而同时按住×、÷时缓慢地按ON则无法开机,于是我做了大量的试验,又发现了许多类似的组合键。当然不同的计算器会有不同的效果,而科学计算器一般不会这样。我当时就在想到底是什么样的程序造成了这样的彩蛋呢?不过到现在这也还是个迷。
前两天在仿制Windows的计算器(以下简称W)时,发现它的算法和平时用的计算器也有很多不同,最明显的就是100+20%并不是100*(1+20%)而是简单的100*20%。为了做到惟妙惟肖我进行的大量的分析。(注:以下分析均指W的标准型。)
为了便于之后的分析我们先来声明一些规则。对于二元运算需要有两个运算数和一个运算符,我们声明W有如下四个变量:左数变量、右数变量、符变量和串变量,分别用于存储左数、右数、运算符(专指四则运算符)和输入的数字字符串。当我们按下四则运算符时将把运算符存入符变量中,当我们输入数字或小数点时,数字将以字符串形式依次存入串变量中。我们称W显示的数字为当前数。至于每个名词的具体含义将在下文用到时给出。
例1:[12.3] 依次将12.3存入串变量(12.3) [+]将+存入符变量(12.3) [45.6] 依次将45.6存入串变量(45.6) [=]计算12.3+45.6并显示结果(57.9)
如例1所示,本文将使用“[按键名]”的形式来表示相应的按键或按下相应的按键,使用 “[按键名] 并发操作(显示)”的格式来表示我们的按键过程及W的运行过程。其中“并发操作”和 “(显示)”有时根据需要会被省略或部分省略。对于连续的数字输入12.3我们将简单的表示为[12.3]而不是[1] [2][.] [3],并称其为一次运算数输入(“一次运算数输入”这一概念在下文中将得到扩展)。若无特别说明每例进行前都要对W进行归零,即[C](0.)。
对于所有按键而言,如果其功能不能正确执行,则发出提示音,如在已[.]的基础上再次[.]。
W提供了八种运算,我们可以将他们分为两类:一元运算[+/-]、[sqrt]、[1/x]和二元运算[+]、[-]、[*]、[/]、[%]。下面我们先来分析最基础的四则运算(即[+]、[-]、[*]和[/])。简单的来说四则运算就是我们想象中的样子,但通过下面的例子我们将会发现,他们也并非我们想象的那样简单。
例2:[1](1.) [+](1.) [=](2.)
例3:[+](0.) [1](1.) [=](1.)
四则运算需要两个运算数及一个运算符(下文若无说明,运算符均指四则运算符),在本文中我们将二元运算符的第一个运算数称为其左数,而第二个运算数称为其右数,并分别用左数变量和右数变量存储这两个数。而例2例3中我们只输入了一个运算数和一个运算符便可以计算结果了。由此我们可以得到这样一个猜想:两个运算数均有初始值。那这个初始值是多少呢?在例1中1+=的结果是2,因此我们可以推断右数的初始值为1。同样的我们可以推断在例3中左数的初始值为0。而如果将这两例中的运算数1换作任意数n,那么我们不难发现例2中右数的初始值实际上是左数,而例3中左数的初始值始终是0。
由此我们可以进一步猜想,在例2中,当我们[=]后,得出结果之前,W已经把我们输入的1同时赋给左右数变量。那这样的赋值运算又是什么时候完成的呢?首先不是[数字](此处理解为按下数字键,请根据语境理解下文中的“[]”格式)时完成的,否则在例3中[1]之后左数也会变为1,而实际上左数为0。其次也不会是[=]时完成的,否则在例3中左数也会变成1,而首次[=]的结果就会变成2。因此赋值运算是在[+]时完成的。那么是不是每次[+]时都会执行这样的赋值运算呢?前面说,W把输入的数字赋给了左右数变量,而在例3中按+之前我们并没有输入任何数字,那么赋值运算是否执行了呢,如果执行了那是将什么数赋给了左右数变量了呢?这里为了简化思路我们不妨假想[+]时赋值运算总是执行的,并且是将当前数赋给了左右数变量。由此我们可以进一步假想左右数变量的初始值都是0,而当前数的初始值显然是0。事实上这两个假想与后面的分析并无矛盾。同理,我们分析[-]、[*]、[/]可以得到相同的结论,不同的只是将[+]换成了[-]、[*]、[/]。
下面我们来分析[=]。由例2例3可以看出,[=]后W计算了表达式 左数 运算符 右数 的结果并显示出来。我们再来看这样的例子:
例4:[3](3.) [*](3.) [10](10.) [=](30.) [=](300.) [=](3000.) [=](30000.)
例5:[3](3.) [*](3.) [10](10.) [=](30.) [=](300.) [11](11.) [=](3000.) [=](30000.)
在例4中,首次[=]计算的表达式应该是 3*10 ,因此结果为30。而根据后三次[=]的结果来看我们可以推测出后三次[=]计算的表达式分别为:30*10、300*10和3000*10。因此我们不难想象,[=]后W计算了表达式 左数 运算符 右数 的结果,并将其赋给左数变量同时显示出来。之前我们已经讨论过了左数变量是在按运算符后被赋值的,那么在例3和这两个例子中右数变量又是什么时候被赋值的呢?不难看出是在[数字]的同时或者是在[=]的同时进行的,那么到底是哪种情况呢?例5将帮助我们找到答案。例5是在例4的基础上多键入了一次数字11,但很明显,这并没有对后面的操作产生影响。如果说按数字键时会把当前数赋给右数变量,那么之后[=]应该得到3300而不是3000。可是同样的道理,如果[=]时进行的赋值,结果也应该是3300而不是3000啊,那赋值到底是怎样进行的呢?
我们再来仔细分析一下例5。在例5中,右数变量可能被赋值的地方有两处,一是[10][=],二是[11][=],那么为什么在第一处将10赋给了右数,而第二处却没有将11赋给右数变量呢?仔细比较我们就会发现它们的区别,第一处的前次是[*],即运算符。而第二处的前次是[=],即非运算符。如果我们称[运算符]后紧接的一次运算数输入为一次有效的右数输入的话,那么当一次有效的右数输入后[=]就把当前数赋给右数变量,否则不进行赋值。这样一来我们又回到了一开始的问题:是在[数字]时还[=]时,才把当前数赋给右数变量的呢?我们先来假设是在[=]时进行赋值的。这样对于例5,W的运行过程就是这样的:
[3][*]将3赋给左右数变量[10][=]由于键入数字键10之前键入了运算符*故将10赋给右数变量并计算3*10,将结果30赋给左数变量同时显示出来[=]计算30*10,并将结果300赋给左数变量同时显示出来[11][=]由于在键入数字键11之前键入了非运算符=故直接计算300*10,并将结果3000赋给左数变量同时显示出来[=]计算3000*10,并将结果30000赋给左数变量同时显示出来。
这样的运行过程并没有什么不妥之处。那么我们再来假设[数字]时把当前数赋给右数变量。这样对于例5,W的运行过程就是这样的:
[3][*]将3赋给左右数变量[10]由于键入数字键10之前键入了运算符*故将10赋给右数变量[=]计算3*10,将结果30赋给左数变量同时显示出来[=]计算30*10,并将结果300赋给左数变量同时显示出来[11]不进行右数变量赋值[=]计算300*10,并将结果3000赋给左数变量同时显示出来[=]计算3000*10,并将结果30000赋给左数变量同时显示出来。
上面的过程乍看之下并无不妥,但是仔细观察我们就会发现“[10]”实际上是“[1][0]”的缩写,因此实际的运行过程应该是:
[3][*]将3赋给左右数变量[1]由于键入数字键1之前键入了运算符*故将当前数1赋给右数变量[0]由于键入数字键0之前键入了非运算符1故不会将当前数10赋给右数变量[=]计算3*1,将结果3赋给左数变量同时显示出来[=]计算3*1,并将结果3赋给左数变量同时显示出来[1]不进行右数变量赋值[1]不进行右数变量赋值[=]计算3*1,并将结果3赋给左数变量同时显示出来[=]计算3*1,并将结果3赋给左数变量同时显示出来。
显然这样的运行过程显然是错误的,因此这个假设是错的。所以当[=]时如果前次为一次有效的右数输入,则将当前数赋给右数变量。
下面我们再来看一个关于[=]的没有什么实际意义的操作:
例6:[12](12.) [=](12.) [45](45.)
由此我们可以看出当[=]时,W会检查符变量是否存在运算符,若不存在,则不作任何运算,且清空串变量,以使之后的输入从初始状态重新开始。
至此,我们已经完成了[=]的分析,下面我们来归纳一下[=]的功能:
1.如果前次为一次有效的右数输入则将当前数赋给右数变量。
2.检查符变量是否存在运算符,若存在则计算表达式 左数 运算符 右数 ,并将结果赋给左数变量,同时显示。 若不存在,则不作操作。
3.清空串变量。
在2.中我们需要注意如果运算符为/,那么我们将分别特殊对待右数为零和左右数同时为零的两种情况,后面我们将进行详细的分析。
下面我们继续来分析四则运算符。先来看下面的例子:
例7:[3](3.) [+](3.) [5](5.) [+](8.) [4](4.) [+](12.) [1](1.)[=](13.)
例8:[3](3.) [+](3.) [5](5.) [=](8.)[+](8.) [4](4.) [=](12.)[+](12.) [1](1.) [=](13.)
例9:[3](3.) [+](3.) [5](5.) [+](8.) [+](8.) [+](8.)
对比例7和例8,我们发现虽然例8中我们多了两次[=],但是结果与例7并无不同。或者说例7中我们少了两次[=],但是同样执行了[=]的功能。由此我们可以认为,当我们[+]时首先执行了[=],然后才执行[+]的功能。而在例9中我们又会发现,最后两次[+]时左数变量、符变量和右数变量分别保存的是8、+和5,而此时并没有执行[=]。那么[+]时什么情况下才执行[=]呢?对比例7和例9我们不难发现,在一次运算数输入后[+],则会执行[=]。当然对于首次键入运算符时是否执行[=]并没有区别,因为[=]不会执行任何运算。同理,我们对于[-]、[*]、[/]进行分析将会得到相同的结论,只是将[+]替换为[-]、[*]、[/]。
至此,我们已经完成了[四则运算符]的分析,下面我们来归纳一下[四则运算符]的功能:
1.若前次为一次运算数输入,则执行[=]。
2.将当前数赋给左右数变量,将运算符赋给符变量。
3.清空串变量。
下面我们分析一元运算符(从此处开始,如无说明运算符均指一元运算符)。
例10:[4](4.) [-](4.) [64](64.) [sqrt](8.) [=](-4.) [+/-](4.) [1/x](0.25 )
一元运算符只需要一个运算数和一个运算符,由例10可以看出这三个运算符都只对当前数进行运算,并将结果显示出来,而不管当前数从何而来。我们还能看出当[=]后,我们是将[sqrt]后得到的8赋给了右数变量。而在前面的分析中我们认为只有一次有效的右数输入后[=],才将当前数赋给右数变量。因此我们可以看出尽管[64][sqrt]中有一次[sqrt],但我们仍可认为它是一次运算数输入,即在例10中它仍是一次有效的右数输入。
下面我们总结一下一元运算符的功能:
0.一次运算数输入可以包括[+/-]、[sqrt]和[1/x]。
1.对当前数进行相应的计算,并显示结果。
2.[sqrt]或[1/x]时清空串变量。
需要注意的是当[sqrt]时应特殊对待当前数为负数的情况,后面我们将进行详细分析。
下面我们来介绍8种运算符中的最后一种%。(注:从此处开始运算符不特指某种运算符,而指所有运算符)
例11:[200](200.) [*](200.) [20](20.) [%](40.) [%](80.)
例12:[100](100.) [-](100.) [5](5.) [%](5.) [%](5.) [%](5.)
例13:[10](10.) [/](10.) [80](80.) [%](8.) [%](0.8 ) [50](50.)[%](5.) [7](7.) [%](0.7 )
例14:[20](20.) [+](20.) [30](30.) [%](6.) [+](26.) [=](52.)
不难看出在例11中第一次[%]计算的是200*20/100,而第二次[%]计算的又是什么呢?我们再看例12,不难看出第一次[%]计算的是100*5/100,而后两次[%]的结果仍然是5。我们再看例13,仍不难看出第一次[%]计算的是10*80/100,这时我们可以得到这样的结论:[%]后的运算不受存在符变量中的运算符的影响。例13中最后两次[%]计算的分别是10*50/100和10*7/100。这时我们便能看出例11中第二次[%]计算的应该是200*40/100,例12中后两次[%]计算的应该100*5/100,例13中第二次[%]计算的应该是10*8/100。根据这三个例子我们可以简单地认为当[%]时W计算了表达式 左数 * 当前数 / 100 。当然我们也可以认为当[%]时,W首先把当前数赋给了右数,然后再计算 左数 * 右数 / 100 。
我们再来看例14,[%]之后的[+]显然通过执行[=]而计算了20+6,所以在[%]之后到计算20+6之前,6被赋给了右数变量。根据之前的分析,当有一次运算数输入后[+]才执行[=],因此,我们可以认为[%]也是一次运算数输入。这样在例14中[30][%]就是一次有效的右数输入,因此我们可以认为6被赋给右数变量是由[+]时执行的[=]来完成的。
下面我们总结一下[%]的功能:
0.一次运算数输入可以包括[%]。
1.计算表达式 左数 * 当前数 / 100 ,并显示结果。
2.清空串变量。
以上就是W的八种运算的相关分析。下面我们来分析其他的按键。首先来分析归零键[C]和[CE]。从表面上看这两个键的功能都是对W归零,但是二者肯定还是有区别的。
例15:[5](5.) [+](5.) [3](3.) [C](0.) [2](2.) [=](0.)
例16:[5](5.) [+](5.) [3](3.) [CE](0.) [2](2.) [=](7.)
对比例15和例16,不难发现当[C]后所有变量均被归零,可以认为W回到了初始状态。而[CE]后虽然当前数被归零,但是左数变量和符变量仍然保留(事实上右数变量也被保留),可以认为W回到了[+]后又[0]的状态,因此[CE]也可以认为是一次运算数输入。
下面我们分析退格键(简写作[BK])。
例17:[2](2.) [5](25.) [+/-](-25.) [BK](-2.) [BK](0.)
[BK]的功能比较简单,可以简单的认为它只是实现了删除串变量的最后一位。从例17中,我们可以看出[BK]也可以认为是一次运算数输入。值得注意的是[BK]只对由[数字][.]和[+/-]产生的数字或通过粘贴直接得到的科学计数法形式的数字(非计算结果)有效,否则[BK]将不执行并发出提示音。不过[BK]在执行功能时W仍要按规矩显示,这就会产生一些问题,这些问题我们将在后面分析显示格式的时再进行讨论。
下面我们分析用于临时存储的四个键。我们先介绍一下这四个键的功能。[MC]:将临时存储(下文简称M)归零;[MR]:显示M;[MS]:将当前数赋给M;[M+]:将 M+当前数 的结果赋给M。同时当M不为0时在[MC]上方的文本框中显示“M”。
例18:[3](3.) [MS](3.) [M+](3.) [+](3.) [1](1.) [MR](6.) [=](9.)
这四个键的功能都较为简单,但由于[MR]会改变当前数,所以仍需要一点分析。在例18中,[=]计算的是3+6而不是3+1,也就是说[=]将[MR]后的6赋给了右数变量。因此我们可以认为[MR]也是一次运算数输入。而由于其余三键均不影响W的运算只是改变了M,所以在分析W的运算时可忽略这三键。
至此W的所有按键就全部分析完了。下面我们来分析菜单中的功能。
在查看菜单中的我们可以将W切换成科学型,本文不对其进行讨论。我们来分析一下查看菜单中数字分组(计作[分])的功能。
例19:[分](0.) [987654321](987,654,321.) [sqrt](31,426.968052931864079255398820135)
由例19我们可以看出,当选中[分]后,数字的整数部分由低位到高位每三位间加一逗号以方便查看。取消[分]后,回复原来的状态。
下面我们来分析编辑菜单中的[复制]和[粘贴]。我们可以简单地认为[复制]和[粘贴]的功能就是将当前数复制到剪贴板和将剪贴板中的数字粘贴到W上显示。仔细的分析后我们会发现,[复制]虽只是将当前数复制到剪贴板,但是[粘贴]却不只能够粘贴数字,它还能够粘贴并计算表达式。由于[复制]对W没有任何影响,因此在W的运行中也均不考虑是否存在[复制]。
例20:复制3+6*3-20/7=
[粘贴](1.)
在例20中不难看出[粘贴]实际上是执行了[3][+][6][*][3][-][20][/][7][=]。因此我们可以认为[粘贴]并不是将剪贴板的字符串一下子粘到W上,而是将字符串从左至右逐字传到W并执行对应按键的功能。与按键对应的字符基本与按键名相同或与对应键盘按键相同。下面列出了W标准型中的对应的键盘按键和对应字符。
W键 |
键盘键 |
字符 |
W键 |
键盘键 |
字符 |
W键 |
键盘键 |
字符 |
0-9 |
0-9 |
0-9 |
+/- |
F9 |
无 |
C |
Esc |
:p |
. |
. or , |
. |
sqrt |
@ |
@ |
CE |
DEL |
无 |
+ |
+ |
+ |
% |
% |
% |
MC |
CTRL+L |
:c |
- |
- |
- |
1/x |
r |
r |
M+ |
CTRL+P |
:p |
* |
* |
* |
= |
Enter |
= |
MR |
CTRL+R |
:r |
/ |
/ |
/ |
Backspace |
Back Space |
无 |
MS |
CTRL+M |
:m |
由于[粘贴]的特殊性,在W的运行中我们要将[粘贴]分解为逐字对应的操作进行分析。
那么在[粘贴]过程中如果传递了以上这些字符之外的字符(下文称其为非法字符)会怎么样呢?
例21: 复制12_34
[粘贴](12)
例22: 复制12d34
[粘贴](1234)传递d时发出提示音
为什么同为非法字符,在例21中W停止了之后字符串的传递,而在例22中却发出提示音后继续传递之后的字符呢?原来例22中的“d”并不是非法字符,而是对应W科学型中16进制数字d的字符。虽然我们在使用W的标准型,但是它并不屏蔽对应科学型的键盘键和字符。因此[粘贴]对非法字符的处理方法是,遇到非法字符便停止非法字符后字符串的传递。而对于合法字符则触发相应的功能,在例22中发出提示音因为,d所触发的功能不能正确执行。
最后我们来分析一下W的三种错误状态。这三种状态分别为:
1.当计算除法时,若除数为0则会显示“除数不能为零。”
2.当计算除法时,若被除数除数同时为0则会显示“函数结果未定义。”
3.当计算开方运算时,若底数为负数则显示“函数输入无效。”
以上三种错误状态无论出现哪一种,W都回被置锁死状态,除[C]和[CE]外的所有功能均不可用。[C]和[CE]都可解除这种锁死状态,不同的是[C]将W回复为初始状态,而[CE]只是解除了锁死状态并将当前数归零。
至此,我们已经完成了W标准型全部功能的分析,下面我们来总结一下。我们曾多次扩展了“一次运算数输入”的概念。所以现在一次运算数输入是指一次或连续多次的[数字][.][一元运算符][%][CE][BK][MR]操作,如存在[粘贴]操作则分解为逐字对应的操作进行处理。
[数字][.]用于输入数字,输入的数字将依次存入串变量。
[=]的功能:
1.如果前次为一次有效的右数输入则将当前数赋给右数变量。
2.检查符变量是否存在四则运算符,若存在则计算表达式 左数 运算符 右数 ,并将结果赋给左数变量,同时显示。 若不存在,则不作操作。
3.清空串变量。
[四则运算符]的功能:
1.若前次为一次运算数输入,则执行[=]。
2.将当前数赋给左右数变量,将运算符赋给符变量。
3.清空串变量。
[一元运算符]的功能:
1.对当前数进行相应的计算,并显示结果。
2.[sqrt]或[1/x]时清空串变量。
[%]的功能:
1.计算表达式 左数 * 当前数 / 100 ,并显示结果。
2.清空串变量。
[C]将W回复到初始状态。[CE]解除锁死状态,将右数变量、串变量和当前数归零。
[BK]删除串变量的最后一个字符。
[MS]用于将当前数存入M中;[M+]将当前数加上M并将结果存入M;[MR]将M显示出来,即成为当前数;[MC]将M归零。这四个按键均会清空串变量。当M不为0时,[MC]上方的文本框显示“M”。
[复制]将当前数复制到剪贴板。[粘贴]将剪贴板字符依次传到W并执行相应按键的功能,若遇到非法字符便停止非法字符后字符串的传递。
[分] 选中状态下数字的整数部分由低位到高位每三位间加一逗号。
W的三种错误状态:
1.右数为0时运算除法显示:“除数不能为零。”
2.左右数同为0时运算除法显示:“函数结果未定义。”
3.当前数为负数时运算开方显示:“函数输入无效。”
出现错误状态后除[C][CE]外所有功能禁用。
有了如上的分析我想仿制一个与W功能相同的计算器就不是什么难事了,但如果想做到惟妙惟肖那还要做很多工作。模仿了功能只是模仿了内在的,虽然这是最主要的,但为了做到惟妙惟肖我们还要模仿W外在的界面。界面和按键的大小、位置及样式是很容易实现的,这里最不做说明了。难于模仿的是W的输出格式,即当前数的显示格式。下面我们进行详细分析。
仔细观察不难发现,除了三种错误状态外,W都将显示一个小数点。三种错误信息均以“。”结束,而非错误状态时当前数的最后一个字符与“。”在同样的位置显示。我们都知道“。”应该占一个全角字符的位置,而非错误状态时所有字符均占一个半角字符的位置,也就是说我们可以认为在非错误状态下,所有显示的字符串的最后一个字符均为半角空格。(不过在下面的分析中为了简化思路我们不将这个空格算在显示的字符串中,并在无在特殊说明时讨论的均为非错误状态下的情况。)
前面说过W将始终显示一个小数点。下面我们来看看不同情况下它是如何显示的。
例23:123.
例24:12.3
例25:1.e+35
例26:1.23e+35
由上面四个例子我们可以看出,在当前数为整数时或科学计数法e前不是小数时小数点显示在整数之后;若有小数存在,小数点显示在其应有的位置上。
我们再来看看由[BK]引发的格式问题。
例27:[-12.34] (-12.34)
[BK] ( -12.3)
[BK] ( -12.)
[BK] ( -12.)
[BK] ( -1.)
[BK] ( 0.)
由例27可以看出在第三次[BK]时虽然从W的显示上看不出变化,但其实这次操作删除了原来串变量中的小数点。而在最后一次[BK]后显示的是(0.)而不是(-.)。
例28:复制1.23e-40
[粘贴] (1.23e-40)
[BK] ( 1.23e-4)
[BK] ( 1.23e+0)
[BK] ( 1.23)
[BK] ( 1.2)
[BK] ( 1.)
[BK] ( 1.)
[BK] ( 0.)
由例28可以看出在第二次[BK]后显示的是(1.23e+0)而不是(1.23e-),而在此时[BK]后将直接显示(1.23)。
最后我们再来看看[粘贴]。由于只有[粘贴]科学计数法数字时W没有与之对应的按键,因此我们来看一下由此引发的格式问题:
例29: 复制 1e
[粘贴](1.e+0)
例30: 复制 1e5
[粘贴](1.e+5)
例31: 复制 1e-
[粘贴](1.e-0)
例32: 复制 1e+
[粘贴](1.e+0)
例33: 复制 222e99.99
[粘贴](222.e+9999)传送.时发出提示音
例34: 复制 777e10000
[粘贴](777.e+1000)传送最后一个0时发出提示音
以上列出了一些较为特殊的例子,需要说明的是在例31和例32中我们可以看出在e之前可以是多位数,而在e之后只能是9999之内的整数,e后的小数点为无效输入。
仔细观察不难发现[粘贴]并不因为计算机的高速性而瞬间完成,而是在传递每个字符之间都有短暂的时间间隔,以至于我们可以看到粘贴时的中间效果。
至此Windows计算器(标准型)的全部分析就结束了。按照以上的分析来仿制Windows计算器(标准型)就一定可以做到惟妙惟肖了吧。