2008-03-22

Trick: Rails里的number_with_precision

关键字: ruby, rails, number_with_precision, number_to_currency

缘起: 很早以前碰到一个需求,实现一个四舍五入(round)的全局HelperMethod,并不难,写出来以后就放在哪里了.而Rails直接提供了一个number_to_currency方法可以方便的在rhtml中将数字显示为CurrencyString,问题来了

helper.number_to_currency(1234567890.50) # => $1,234,567,890.50
如果对这个数字31.825执行转换呢?
helper.number_to_currency(31.825) #=> "$31.82"
结果不对呀,应该是"$31.83"才对,继续对数字32.825执行转换
helper.number_to_currency(32.825) #=> "$32.83"

这次结果又是对了,怪不怪?

 

我当前正在使用的平台是Ubunt 7.10, ruby 1.8.6, rails 2.02

ActionView::Helpers::NumberHelper#number_to_currency方法中的round过程其实是通过ActionView::Helpers::NumberHelper#number_with_precision方法完成的
方法实现非常简单,仅仅用到了String的格式化输出
"%01.#{precision}f" % number
Rails文档上的example写到
number_with_precision(111.2345) # => 111.235
可实际执行结果并不是这样
helper.number_with_precision(111.2345) #=> "111.234"
在官方的Rails Trac中也有人提到这个问题,且看来问题由来已久,见[trick 10090],[trick 8275]
对问题的看法大概分为两个方面,一方认为文档写错了,修改文档.另一方认为的确是计算结果不对,但不是rails的错,错在ruby的String格式化输出,而深入研究则发现这个问题源自C语言的格式化字符串上:
$ gcc -v
gcc version 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)

# 编译下面C代码
# include <stdio.h>

main()
{
    printf("%01.2f\n", 31.825);
    printf("%01.2f\n", 32.825);
}

# 我自己编译后输出结果的确是如此:
31.82
32.83
有人提出用BigDecimal替换Float数据类型
"%01.20f" % BigDecimal.new("31.825") #=> "31.82"
"%01.20f" % BigDecimal.new("32.825") #=> "32.82"
在我的平台上还不如用Float来计算,用Float至少我还有对的时候,但是其他平台上难说,因为String#%方法是C语言实现,具有平台依赖性.
 
其实终端控制台Console下的Bash本身也支持printf输出,且计算结果没有任何问题!
$ bash -version
GNU bash, version 3.2.25(1)-release (i486-pc-linux-gnu)
Copyright (C) 2005 Free Software Foundation, Inc.

$ printf "%01.2f\n" 31.825
31.83
$ printf "%01.2f\n" 32.825
32.83
 
我必须在rails中处理一些严肃的计算任务,必须严格按照正确的四舍五入计算方法处理并显示结果,比如实时显示税率计算结果等
有人提出是因为Float本身精度限制
"%01.20f" % 31.825 #=> "31.82499999999999928946"
只要计算过程中给初始数字加权一个tiny数即可绕过Float的精度问题,为了尽量不影响精度,这个tiny数我取0.1的(precision^precision)次方
def number_with_precision(number, precision=2)
  "%01.#{precision}f" % (number + 0.1 ** (precision ** precision))
end

number_with_precision(31.825) #=> "31.83"
的确是个办法,但是有一点小问题,如果我想要6位精度,那么
number_with_precision(31.0000005,6) #=> "31.000000"
加权计算的tiny数太小了,不足以影响Float精度,但是太大初始数可能会不对,如何聪明的判断加权的tiny数是刚好呢?

目前我用了这样的一个算法:
def number_with_precision(number, precision=3)
  "%01.#{precision}f" % ((Float(number)*(10**precision)).round.to_f/10**precision)
rescue
  number
end

number_with_precision(31.825, 2) #=> "31.83"
number_with_precision(32.825, 2) #=> "32.83"
number_with_precision(32.0000005, 6) #=> "31.000001"
这个方法实现比较繁琐,先改变初始数小数点位置,后移precision个位置,然后用round四舍五入掉小数点后一位,在改变小数点尾数到初始状态,最后结果再用String格式化输出,保证小数点后精度的有效位.不知还有没有比较方便简洁一点的方法?
评论
lgn21st 2008-03-25
今天早上真兴奋,开心~~~
通过元一同学的大力帮助,测试,修改,这个number_with_precision已经被Rails-Core正式接受了,太棒了,谢谢所有人帮助测试~~~

ChangeSet: http://dev.rubyonrails.org/changeset/9086
Ticket: http://dev.rubyonrails.org/ticket/11409

再接再厉,努力学习,打好基础,为社区多多作贡献!
发表评论

您还没有登录,请登录后发表评论

lgn21st
搜索本博客
存档
最新评论