Java中Decimal除法比例求和无法得1的问题及Google Sheets实现疑问
计算比例求和无法得到1的问题解决
用Java计算各数值占总和的比例后求和,结果始终不等于1,但在Google Spreadsheet中执行相同计算却能得到1。相关数据如下:
| A | B | C | D | SUM |
|---|---|---|---|---|
| 0.3 | 0.33 | 0.333 | 0.3333 | 1.2963 |
| 0.231427910205971 | 0.254570701226568 | 0.256884980328628 | 0.257116408238834 | 1 |
想了解Google Spreadsheet的BigDecimal运算逻辑,以及如何在Java中实现比例求和为1的效果,测试代码如下:
import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.math.RoundingMode; public class TestTest { @Test public void testI() { RatioTest a = new RatioTest("0.3"); RatioTest b = new RatioTest("0.33"); RatioTest c = new RatioTest("0.333"); RatioTest d = new RatioTest("0.3333"); BigDecimal sum = a.getAmount().add(b.getAmount()).add(c.getAmount()).add(d.getAmount()); System.out.println("sum : " + sum); a.setRatio(a.amount.divide(sum, 15, RoundingMode.FLOOR)); b.setRatio(b.amount.divide(sum, 15, RoundingMode.FLOOR)); c.setRatio(c.amount.divide(sum, 15, RoundingMode.FLOOR)); d.setRatio(d.amount.divide(sum, 15, RoundingMode.FLOOR)); System.out.println("a : " + a); System.out.println("b : " + b); System.out.println("c : " + c); System.out.println("d : " + d); System.out.println("ratio sum : " + a.getRatio().add(b.getRatio()).add(c.getRatio()).add(d.getRatio())); } private static class RatioTest { private BigDecimal amount; private BigDecimal ratio; public RatioTest(String amount) { this.amount = new BigDecimal(amount); } public BigDecimal getAmount() { return amount; } public BigDecimal getRatio() { return ratio; } public void setRatio(BigDecimal ratio) { this.ratio = ratio; } @Override public String toString() { return "RatioTest{" + "amount=" + amount + ", ratio=" + ratio + '}'; } } }
即使更换a.amount.divide(sum, 15, RoundingMode.FLOOR)中的RoundingMode,比例求和结果仍不等于1。
Google Spreadsheet的运算逻辑
Google Sheets在计算比例时,不会单独对每个比例做舍入后直接相加,而是先计算所有比例的精确值,再对前n-1个比例进行舍入,最后用1减去前n-1个舍入后比例的总和作为最后一个比例的值,以此强制保证所有比例的总和为1,规避了舍入误差的累积问题。
Java中的实现方案
方案一:调整最后一个比例
计算前n-1个比例的舍入值,最后一个比例用BigDecimal.ONE减去前面所有比例的和,确保总和为1。修改后的测试代码如下:
import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.math.RoundingMode; public class TestTest { @Test public void testI() { RatioTest a = new RatioTest("0.3"); RatioTest b = new RatioTest("0.33"); RatioTest c = new RatioTest("0.333"); RatioTest d = new RatioTest("0.3333"); BigDecimal sum = a.getAmount().add(b.getAmount()).add(c.getAmount()).add(d.getAmount()); System.out.println("sum : " + sum); // 计算前三个比例的舍入值 a.setRatio(a.amount.divide(sum, 15, RoundingMode.HALF_UP)); b.setRatio(b.amount.divide(sum, 15, RoundingMode.HALF_UP)); c.setRatio(c.amount.divide(sum, 15, RoundingMode.HALF_UP)); // 最后一个比例用1减去前三个的和 BigDecimal sumOfFirstThree = a.getRatio().add(b.getRatio()).add(c.getRatio()); d.setRatio(BigDecimal.ONE.subtract(sumOfFirstThree)); System.out.println("a : " + a); System.out.println("b : " + b); System.out.println("c : " + c); System.out.println("d : " + d); System.out.println("ratio sum : " + a.getRatio().add(b.getRatio()).add(c.getRatio()).add(d.getRatio())); } private static class RatioTest { private BigDecimal amount; private BigDecimal ratio; public RatioTest(String amount) { this.amount = new BigDecimal(amount); } public BigDecimal getAmount() { return amount; } public BigDecimal getRatio() { return ratio; } public void setRatio(BigDecimal ratio) { this.ratio = ratio; } @Override public String toString() { return "RatioTest{" + "amount=" + amount + ", ratio=" + ratio + '}'; } } }
方案二:保留精确比例再按需调整
如果不需要提前舍入,可以先保留所有比例的精确值(不指定小数位数),此时相加总和必然为1。当需要展示或存储固定小数位时,再对最后一项进行调整,保证总和符合要求。
注意事项
单独对每个比例进行舍入后,由于舍入误差的累积,总和必然会偏离1,这是数学上的正常现象。Google Sheets通过调整最后一项的方式规避了这个问题,Java中可以借鉴相同的逻辑来实现需求。
内容的提问来源于stack exchange,提问作者SANGCO
相关产品推荐
相关产品推荐

