luochen1990.me

Visit My GitHub

JavaScript中对数值取N位有效数字

问题

问题很简单,就是设计一个函数,给定n和x,返回对浮点数x取n位有效数字的结果。(隐含要求是:返回的结果被打印出来应该是reasonable的)

函数签名如下:

precise = (n) -> (x) -> x'

这个函数的两个参数被我设计成了科里化的形式,因为precise(n)返回一个函数,这个函数的语义是“对参数x取n位有效数字”,这个语义还是比较明确的。

实现

这个问题首先能想到有三种解决方法,按照解决方案从优到劣依次为:

  1. 查Math看有没有内置方法可以直接解决
  2. 自己通过数值计算解决
  3. 通过先转字符串处理,取到两位有效数字后再转回数值

然后,开始爬坑了:

首先,浏览了一遍Math的API,没有发现可以直接解决问题的东西。

于是,打算自己通过数值计算实现一个,然后,就写了下面的代码:

precise = (n) -> (x) ->
	r = floor(Math.log10(x))
	a = (10 ** (n - 1 - r))
	floor(x * a) / a

这里可以试运行

但是,发现有些数据打印出的结果会是这样:

## precise(1)(100000) ==> 99999.99999999999

虽然说,数值计算必然会有误差,但是,如果结果是要显示为一个reasonable的数值的话,这种情况还是无法接受。

分析以上代码,发现如果想要结果reasonable的话,依赖这样一个假设:floor(x * a) / a(即 整数 / 小数)的结果都是reasonable的。但是我们发现:

## 1 / 0.00001 ==> 99999.99999999999

所以这个假设是不成立的。

然后,尝试把除法改为乘法调整为这个样子:

precise = (n) -> (x) ->
	r = floor(Math.log10(x))
	a = (10 ** -(n - 1 - r))
	floor(x / a) * a

这里可以试运行

虽然上面那个测试样例通过了,但是总觉得这个方法正确的可能性不大,所以又试了很多测试数据,终于被我找到了bug:

## precise(1)(0.0000052345) ==> 0.0000049999999999999996

另外,其中的floor(Math.log10(x))这一步其实也是有问题的,这里做了个测试,测试发现,小于但是十分接近10的整数次幂的数,处理时会出现精度问题。(当然Math.log10还有一些浏览器兼容问题,但都可以解决,问题不大)

最终,发现想通过数值计算实现这个需求似乎走不通,因为不管怎么样,要得到结果最后一步都需要经过浮点数计算,而一旦进行浮点数计算就会产生精度损失,然后就会产生一个unreasonable的结果。

正当我打算走“先转字符串处理”这条路的时候,@文祎骁 同学帮我发现了toExponential这个好东西,当然用toPrecision也一样。

于是最后这样子就实现了:

precise = (n) -> (x) ->
	parseFloat x.toPrecision(n)

这里可以试运行

结论

  1. JavaScript似乎能保证reasonable的数值字面值被解析为浮点数之后,再toString成字符串还是reasonable的;但是不能保证两个reasonable的数值字面值被解析为浮点数之后,进行(加减乘除等)浮点运算的结果,toString成字符串后,仍然是reasonable的。这点比较有意思。
  2. 都怪自己看文档不仔细 23333333