小墨の博客

梦想需要付诸行动,否则只能是梦

算金额 / 传雪花ID时遇到的数字精度问题整理(前端及后端)

关键词:四舍五入、向下舍入、精度问题、前端、后端、前后端

相关问题:金额计算问题,雪花id精度丢失问题

目录:

后端 BigDecimal 四舍五入报错

前端 toFixed 既不是四舍五入,也不是向下或向上舍入


后端

后端总体来说还好,主要整理一下 BigDecimal 四舍五入问题。

BigDecimal 四舍五入未指定舍入方式

在开发中遇到了同事写的一个代码报错,是 BigDecimal 四舍五入保留两位小数时未指定舍入方式导致的。

话不多说,上问题代码(源代码逻辑比较复杂,新写了一个等效代码):

import java.math.BigDecimal;

public class Example {
    public static void main(String[] args) {
        BigDecimal number = new BigDecimal("3.1456");
        BigDecimal roundedNumber = number.setScale(2); // 如果小数位数大于2位,这里会报错
        System.out.println(roundedNumber);
    }
}

于是乎,问题就出现在 number.setScale(2) 这里了,看报错:

java.lang.ArithmeticException: Rounding necessary
	at java.math.BigDecimal.commonNeedIncrement(BigDecimal.java:4148) ~[na:1.8.0_191]
	at java.math.BigDecimal.needIncrement(BigDecimal.java:4204) ~[na:1.8.0_191]
	at java.math.BigDecimal.divideAndRound(BigDecimal.java:4112) ~[na:1.8.0_191]
	at java.math.BigDecimal.setScale(BigDecimal.java:2452) ~[na:1.8.0_191]
	at java.math.BigDecimal.setScale(BigDecimal.java:2512) ~[na:1.8.0_191]
    .....

解决方法也很简单,setScale(2) 改成 setScale(2, RoundingMode.HALF_UP) 就好了,上代码:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Example {
    public static void main(String[] args) {
        BigDecimal number = new BigDecimal("3.1456");
        BigDecimal roundedNumber = number.setScale(2, RoundingMode.HALF_UP);
        System.out.println(roundedNumber);
    }
}

再次完整复现下问题:

public static void main(String[] args) {
	BigDecimal bigDecimal;
	String string;

	bigDecimal = new BigDecimal("3.1");
	// 正常 不推荐(idea提示WARNING)
	string = bigDecimal.setScale(2).toString();
	// 正常 不推荐(idea提示WARNING)
	string = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).toString();
	// 正常 推荐
	string = bigDecimal.setScale(2, RoundingMode.HALF_UP).toString();

	bigDecimal = new BigDecimal("3.141");
	// 报错 不推荐(idea提示WARNING)
	try {
		string = bigDecimal.setScale(2).toString();
	} catch (ArithmeticException e) {
		e.printStackTrace();
	}
	// 正常 不推荐(idea提示WARNING)
	string = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).toString();
	// 正常 推荐
	string = bigDecimal.setScale(2, RoundingMode.HALF_UP).toString();
}

关于 RoundingMode,网上是这么说的,可以参考一下:

Java 中的 BigDecimal 类中的 RoundingMode 属性用于设置舍入模式。该属性可以取以下枚举值:

  • RoundingMode.UP: 向远离零的方向舍入

  • RoundingMode.DOWN: 向接近零的方向舍入

  • RoundingMode.CEILING: 向正无穷方向舍入

  • RoundingMode.FLOOR: 向负无穷方向舍入

  • RoundingMode.HALF_UP: 向最近的整数,如果距离两边相等则向上舍入

  • RoundingMode.HALF_DOWN: 向最近的整数,如果距离两边相等则向下舍入

  • RoundingMode.HALF_EVEN: 向最近的整数,如果距离两边相等则向偶数舍入

  • RoundingMode.UNNECESSARY: 不进行舍入操作,如果存在非精确结果则抛出 ArithmeticException 异常


主键雪花ID太长导致前端 JSON.parse() 时后几位精度丢失

其实这时前端的问题,但是前端不好解决,所以从后端SpringBoot接口返回入手解决,将雪花id从Long转成String即可解决。

可以按照 mybatis-plus 官网说的方法解决,下面摘录一下解决方法。

原文档地址:ID_WORKER 生成主键太长导致 js 精度丢失

JavaScript 无法处理 Java 的长整型 Long 导致精度丢失,具体表现为主键最后两位永远为 0,解决思路: Long 转为 String 返回

FastJson 处理方式

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter();
    FastJsonConfig fjc = new FastJsonConfig();
    // 配置序列化策略
    fjc.setSerializerFeatures(SerializerFeature.BrowserCompatible);
    fastJsonConverter.setFastJsonConfig(fjc);
    converters.add(fastJsonConverter);
}

JackJson 处理方式

方式一

// 注解处理,这里可以配置公共 baseEntity 处理
@JsonSerialize(using=ToStringSerializer.class)
public long getId() {
    return id;
}

方式二

// 全局配置序列化返回 JSON 处理
final ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);

比较一般的处理方式:增加一个 public String getIdStr() 方法,前台获取 idStr


前端

前端的坑可就多了,肯定还有我没整理到的,大家如果有遇到,可以顺手在下面评论一下,我会整理进来,感谢!


金额千分位

JavaScript 数字添加千分位问题我之前整理过,见:https://www.only4.work/blog/?id=514


Number.toFixed() 不是四舍五入,也不是向下舍入

开始之前,先说个题外话:

四舍五入有个小技巧,不记得是在哪本书上看到的(不过那本书作者好像也说得是不记得在哪学到的了(笑))。

保留整数四舍五入 等价于 加 0.5 后向下舍入

保留一位小数四舍五入 等价于 加 0.05 后向下舍入


不能使用toFixed(x)去进行舍入操作,因为IEEE754标准规定的浮点数取整算法是银行家舍入法,即四舍六入五留双法:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。


关于浮点数值计算会产生摄入误差的问题,有一点需要明确:这是使用基于IEEE754数值的浮点计算的通病,ESMAScript并非独此一家,其他使用相同数值格式的语言也存在这个问题。

——《JS高级程序设计(第3版)》3.4.5 Number类型


说完了,下面复现一下 toFixed() 的问题。测试代码:

var numList = [
    1.44, 1.45,
    2.44, 2.45,
    1.55, 2.55, 3.55, 4.55,
    2.65, 3.65,
    -3.5
]

// numList.forEach(num => console.log(num, num.toFixed(1)))
numList.forEach(num => {
    // 保留一位小数四舍五入 = 加 0.05 后向下舍入
    let round = Math.floor(num * 10 + 0.5) / 10
    // 前端 Number.toFixed 方法
    let toFixed = +num.toFixed(1)
    // 前端 Math.round 方法
    let mathRound = Math.round(num * 10) / 10
    console.log(num,
        'round:', round,
        'mathRound:', mathRound, ...round !== mathRound ? ['round !== mathRound'] : [],
        'toFixed:', toFixed, ...round !== toFixed ? ['round !== toFixed'] : [])
})


image.png

BTW(顺便说一句),Math.round() 以及其他的一些 Math 方法可能会有精度问题。


前端的一些坑

let testCase = [
    () => 0.07 * 100, // 7.000000000000001
    () => 0.1 + 0.2, // 0.30000000000000004
    () => 2.3 + 2.4, // 4.699999999999999
    () => [Math.round(-3.5), (-3.5).toFixed(0), -3.5.toFixed(0)], // [-3, '-4', -4]
    () => [Math.round(3.5), 3.5.toFixed(0), +3.5.toFixed(0)], // [4, '4', 4]
    () => [-'4', +'4'], // [-4, 4]
    () => [+'+4', +'-4', -'+4', -'-4'], // [4, -4, -4, 4]
    () => [+'+0', +'-0', -'+0', -'-0'], // [0, -0, -0, 0]
]
for (let t of testCase) {
    console.log(t, t())
}

image.png


前端数字计算相关轮子

前端感觉还比较复杂,感觉坑可能会比较多,所以个人觉得,如果是上生产环境,可能使用别人封装好的轮子会比自己把问题全踩一遍更快(更能保住工作哈哈),如果是自己研究学习的话,倒是可以多研究研究,看看有什么坑,这样之后同事遇到同样问题,就可以第一眼发现然后无情嘲笑他了(嘿嘿,我真坏)


在网上搜了一下相关轮子,但是没有实际验证过,只是简单看了下文档和GitHub,列出了最近有更新的,如下:


currency.js

A small, lightweight javascript library for working with currency values.

GitHub: https://github.com/scurker/currency.js

官网: https://currency.js.org/

npm: https://www.npmjs.com/package/currency.js

前端处理金额精度问题和千分位格式化

https://www.jianshu.com/p/df0f6583e405


math.js

An extensive math library for JavaScript and Node.js

GitHub: https://github.com/josdejong/mathjs

官网: https://mathjs.org/

npm: https://www.npmjs.com/package/math.js


decimal.js

An arbitrary-precision Decimal type for JavaScript.

官网: 

GitHub: https://github.com/MikeMcl/decimal.js

npm: https://www.npmjs.com/package/decimal.js


bignumber.js

A JavaScript library for arbitrary-precision decimal and non-decimal arithmetic.

官网: 

GitHub: https://github.com/MikeMcl/bignumber.js

npm: https://www.npmjs.com/package/bignumber.js



本文由张小弟之家原创,转载请注明出处(blog.only4.work)并在本文下方评论下转载地址,感谢~

张小弟之家

本文链接:
文章标题:

本站文章除注明转载/出处外,均为原创,若要转载请务必注明出处。转载后请将转载链接通过邮件告知我站,谢谢合作。本站邮箱:admin@only4.work

尊重他人劳动成果,共创和谐网络环境。点击版权声明查看本站相关条款。

    发表评论:

    搜索
    本文二维码
    标签列表
    站点信息
    • 文章总数:507
    • 页面总数:20
    • 分类总数:92
    • 标签总数:207
    • 评论总数:60
    • 浏览总数:219202

    | | |
    | |  Z-Blog PHP