Clojure 浮点数运算
BigDecimal
浮点数虽然表示的范围大,但常常无法精确表示。比如 (+ 0.1 0.2) 结果并不是预期的 0.3,而是 0.30000000000000004,有一个网站http://0.30000000000000004.com/解释了产生这种情况的原因以及在各个语言中的表现。
在Clojure中为了精确表示数值运算(比如金额计算),可以借助于Java中的 BigDecimal来处理。
BigDecimal提供了三个构造方法来创建一个BigDecimal对象,优先建议使用String构造方法,因为String构造方法是完全可预知的,如(bigdec "0.1")。
BigDecimal提供了相应的加减乘除方法,其参数也必须为BigDecimal类型。
public BigDecimal add(BigDecimal value); // 加法
public BigDecimal substract(BigDecimal value); // 减法
public BigDecimal multiply(BigDecimal value); // 乘法
public BigDecimal divide(BigDecimal value); // 除法
需要注意的是除法运算,如果BigDecimal除法出现不能整除的情况,会抛java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.异常。比如(.divide (bigdec "0.5") (bigdec "0.3"))。
解决方法可以使用divide的重载方法,此方法可以接收三个参数,第一个参数表示除数,第二个参数表示小数点后保留位数,第三个参数表示舍入模式。
public BigDecimal divide(BigDecimal divisor, int scale int roundingMode)
示例:计算 0.5 除以 0.3,保留2位小数,采用向下取数
(.divide (bigdec "0.5") (bigdec "0.3") 2 BigDecimal/ROUND_DOWN) ;; 结果为 1.66M
舍入模式只在做除法或四舍五入时才会用到,有以下几种:
ROUND UP:向远离0的方向舍入
ROUND_DOWN:向零方向舍入
ROUND_CEILING:向正无穷方向舍入
ROUND_FLOOR:向负无穷方向舍入
ROUND_HALF_UP:向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向上舍入, 1.55保留一位小数结果为1.6(四舍五入)
ROUND_HALF_DOWN:向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向下舍入, 例如1.55 保留一位小数结果为1.5
ROUND_HALF_EVEN:向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,如果保留位数是奇数,使用ROUND_HALF_UP,如果是偶数,使用ROUND_HALF_DOWN
ROUND_UNNECESSARY:计算结果是精确的,不需要舍入模式
舍入模式示例:
| 示例值 | UP | DOWN | CEILING | FLOOR | HALF_UP | HALF_DOWN | HALF_EVEN | UNNECESSARY |
|---|---|---|---|---|---|---|---|---|
| 5.5 | 6 | 5 | 6 | 5 | 6 | 5 | 6 | throw ArithmeticException |
| 2.5 | 3 | 2 | 3 | 2 | 3 | 2 | 2 | throw ArithmeticException |
| 1.6 | 2 | 1 | 2 | 1 | 2 | 2 | 2 | throw ArithmeticException |
| 1.1 | 2 | 1 | 2 | 1 | 1 | 1 | 1 | throw ArithmeticException |
| 1.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| -1.0 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 |
| -1.1 | -2 | -1 | -1 | -2 | -1 | -1 | -1 | -1 |
| -1.6 | -2 | -1 | -1 | -2 | -2 | -2 | -2 | throw ArithmeticException |
| -2.5 | -3 | -2 | -2 | -3 | -3 | -2 | -2 | throw ArithmeticException |
| -5.5 | -6 | -5 | -5 | -6 | -6 | -5 | -6 | throw ArithmeticException |
BigDecimal提供了一个 setScale 方法,用来对数据进行舍入处理。
(.setScale (bigdec "1.256332") 2 BigDecimal/ROUND_DOWN) ;; 结果为 1.25M
with-precision方法
Clojure 提供了with-precision方法可以很方便的进行浮点数格式化处理
(with-precision 2 :rounding HALF_DOWN (/ 2M 11)) ;=> 0.18M
使用此方法时要特别注意以下几点:
参与计算的数值必须为 BigDecimal,如果为 float 或 double 则会报错。一个 BigDecimal 与 一个整数不会报错
舍入模式与上表中的舍入模式相同,默认为
HALF_UP精度数 (示例中的 2),并代表保留2位小数,而是表示有效数字位数(如果整数位不为0,则也会算入)
(with-precision 2 :rounding HALF_DOWN (/ 20M 11)) ;=> 1.8M查看其源码可以发现
with-precision方法实际使用的是java.math.MathContext,它有两个属性precision:某个操作使用的数字个数;结果传入到此精度**(并不是小数位数)**roundingMode:舍入模式
Clojure分数ratio使用
为了避免浮点数精度问题,clojure中提供了分数类型,在计算过程中全都使用分数计算(特别是大量除法运算时),得到最终结果后,在对分数求值保存,最大程度减少精度损失。
(/ 1 2) ;=> 1/2
(type (/ 1 2)) ;=> clojure.lang.Ratio
(+ (/ 1 2) (/ 22 7)) ;=> 51/14
可以使用 rationalize将小数转换为分数
(rationalize 0.2) ;=> 1/5
(+ (rationalize 0.2) (rationalize 0.1)) ;=> 3/10,避免了 0.1+0.2 != 0.3 的问题
(rationalize (+ 0.1 0.2)) ;=> 7500000000000001/25000000000000000
将分数转换为小数,并没有找到合适的方法,如果分数是可以整除的,可以转换为 BigDecimal,如果不能整除,转换为 BigDecimal 时会抛异常,可以转换为 float 或 double
(bigdec 1/10) ;=> 0.1M
(bigdec 2/11) ;=> java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
(float 2/11) ;=> 0.18181819
(double 2/11) ;=> 0.1818181818181818