Java和SQL Server中的精确噩梦

我一直在努力解决Java和SQL Server中的精确噩梦,直到​​我不知道为止。 就个人而言,我理解这个问题及其根本原因,但向全球客户解释这一点是不可行的(至少对我而言)。

情况就是这样。 我在SQL Server中有两列–Qty INT和Price FLOAT。 这些值为 – 1250和10.8601 – 因此,为了获得总值,其数量*价格和结果为13575.124999999998(在Java和SQL Server中)。 那是对的。 问题是这个 – 客户端不想看到它,他们只看到这个数字为13575.125就是这样。 在一个地方,他们可以用2位小数精度看到它,另外4位小数。 当以4位小数显示时,数字是正确的 – 13575.125,但是当以2位小数显示时,他们认为它是错误的 – 13575.12 – 应该是13575.13!

帮帮我。

你的问题是你正在使用浮动。 在java方面,你需要使用BigDecimal,而不是float或double,在SQL端你需要使用Decimal(19,4)(如果它有助于跳转到你的精度级别,则使用Decimal(19,3))。 不要使用Money类型,因为SQL中的Money类型的数学会导致截断,而不是舍入。 数据存储为float类型(你说它是不可更改的)的事实不会影响这一点,你只需要在第一次机会之前转换它,然后再对它进行数学运算。

在给出的具体示例中,您需要首先获取4位小数精度数,并根据具体情况将其放在BigDecimal或Decimal(19,4)中,然后进一步将其舍入为2位小数精度。 然后(如果你正在四舍五入)你将得到你想要的结果。

使用BigDecimal 。 Float不是代表金钱的代理类型。 它将正确处理舍入。 Float将始终产生舍入误差。

对于存储货币金额,浮点值不是要走的路。 根据您的描述,我可能会将金额作为长整数来处理货币金额乘以10 ^ 5作为数据库存储格式。

您需要能够使用不会丢失精度的数量来处理计算,因此这里再次浮动点不是可行的方法。 如果借方和贷方之间的总和在分类账中偏差1美分,则分类账在财务人员眼中失败,因此请确保您的软件在问题域中运行,而不是您的。 如果您不能将现有类用于货币金额,则需要构建自己的类,其amount * 10^5并且根据仅用于输入和输出目的的精度进行格式化。

不要将float数据类型用于价格。 你应该使用“Money”或“SmallMoney”。

这是[MS SQL DataTypes] [1]的参考。

[1]: http : //webcoder.info/reference/MSSQLDataTypes.html

更正:使用十进制(19,4)

谢谢Yishai。

我想我看到了问题。

10.8601无法完美表示,因此虽然向13575.125的舍入工作正常但很难将其舍入到.13,因为添加0.005只是不能完全实现。 更糟糕的是,0.005也没有精确的表示,所以最终只有0.13的差距。

然后你的选择要么是两次,一次是三位数,一次是2次,要么做一个更好的计算。 使用长或高精度格式,缩放1000以获得* .125到* 125。 使用精确整数进行舍入。

顺便说一句,说“浮点不准确”或者它总是产生错误的无休止重复变化之一并不完全正确。 问题是格式只能表示可以将2的负幂加总来创建的分数。 因此,在序列0.01到0.99中,只有.25,.50和.75具有精确的表示。 因此,最好使用FP,具有讽刺意味的是,通过缩放它以便只使用整数值,然后它与整数数据类型算法一样准确。 当然,那么你可能刚开始使用定点整数。

小心,缩放,比方说,0.37到37仍然不准确,除非四舍五入。 浮点数用于货币价值,但它的工作量超过其价值,通常无法获得必要的专业知识。

如果您无法修复底层数据库,可以像这样修复java:

 import java.text.DecimalFormat; public class Temp { public static void main(String[] args) { double d = 13575.124999999; DecimalFormat df2 = new DecimalFormat("#.##"); System.out.println( " 2dp: "+ Double.valueOf(df2.format(d)) ); DecimalFormat df4 = new DecimalFormat("#.####"); System.out.println( " 4dp: "+Double.valueOf(df4.format(d)) ); } } 

虽然您不应该首先将价格存储为float ,但您可以考虑将其转换为decimal(38, 4) ,比如说或money (请注意,由于涉及它的表达式的结果没有,因此存在一些问题他们的规模动态调整),并在一个视图中暴露出SQL Server的出路:

 SELECT Qty * CONVERT(decimal(38, 4), Price) 

因此,鉴于您无法更改数据库结构(这可能是最佳选择,因为您使用非固定精度来表示应该修复/精确的内容,正如许多其他人已经讨论过的那样),希望你可以在某处更改代码。 在Java方面,我认为像@andy_boot这样的回答会起作用。 在SQL方面,你基本上需要将非精确值转换为你需要的最高精度并继续从那里抛出,基本上是这样的SQL代码:

 declare @f float, @n numeric(20,4), @m money; select @f = 13575.124999999998, @n = 13575.124999999998, @m = 13575.124999999998 select @f, @n, @m select cast(@f as numeric(20,4)), cast(cast(@f as numeric(20,4)) as numeric(20,2)) select cast(@f as money), cast(cast(@f as money) as numeric(20,2)) 

您也可以使用DecimalFormat然后使用它进行舍入。

 DecimalFormat df = new DecimalFormat("0.00"); //or "0.0000" for 4 digits. df.setRoundingMode(RoundingMode.HALF_UP); String displayAmt = df.format((new Float()).doubleValue()); 

我同意其他人的意见,你不应该使用Float作为DB字段类型来存储货币。

如果无法将数据库更改为固定的十进制数据类型,则可以尝试通过截断((x + .0055)* 10000)/ 10000进行舍入。 然后1.124999将“舍入”到1.13并给出一致的结果。 在数学上这是不可靠的,但我认为它适用于你的情况。

FLOAT数据类型不能准确表示分数,因为它是base2而不是base10。 (参见方便的链接:) http://gregs-blog.com/2007/12/10/dot-net-decimal-type-vs-float-type/ )。

对于财务计算或需要准确表示分数的任何内容,必须使用DECIMAL数据类型。