double数值使用string.Format()返回的结果不准确。

4

我有这段代码:

<tbody>
    @foreach (var day in user.WorkDays)
    {
        <tr>
            <th>@day.Date.ToString("MM/dd/yy")</th>
            <td>
                <ul>
                    @foreach (var note in day.Notes)
                    {
                        <li>@note.Text</li>
                    }
                </ul>
            </td>
            <td>@string.Format("{0:0.00}", Math.Truncate(day.Totals.Regular * 100) / 100)</td>
            <td>@string.Format("{0:0.00}", Math.Truncate(day.Totals.Overtime * 100) / 100)</td>
            <td>@string.Format("{0:0.00}", Math.Truncate(day.Totals.Doubletime * 100) / 100)</td>
            <td>@string.Format("{0:0.00}", Math.Truncate(day.Totals.Sick * 100) / 100)</td>
            <td>@string.Format("{0:0.00}", Math.Truncate(day.Totals.Vacation * 100) / 100)</td>
            <td>@string.Format("{0:0.00}", Math.Truncate(day.Totals.Holiday * 100) / 100)</td>
            <td>@string.Format("{0:0.00}", Math.Truncate(day.Totals.Overall * 100) / 100)</td>
        </tr>
    }
</tbody>
<tfoot>
    <tr>
        <th>Totals:</th>
        <th></th>
        <th>@string.Format("{0:0.00}", Math.Truncate(user.Totals.Regular * 100) / 100)</th>
        <th>@string.Format("{0:0.00}", Math.Truncate(user.Totals.Overtime * 100) / 100)</th>
        <th>@string.Format("{0:0.00}", Math.Truncate(user.Totals.Doubletime * 100) / 100)</th>
        <th>@string.Format("{0:0.00}", Math.Truncate(user.Totals.Sick * 100) / 100)</th>
        <th>@string.Format("{0:0.00}", Math.Truncate(user.Totals.Vacation * 100) / 100)</th>
        <th>@string.Format("{0:0.00}", Math.Truncate(user.Totals.Holiday * 100) / 100)</th>
        <th>@string.Format("{0:0.00}", Math.Truncate(user.Totals.Overall * 100) / 100)</th>
    </tr>
</tfoot>

它产生了这个结果: enter image description here (在新标签页中打开图片以查看完整大小。)
如果你看右侧列上的值,你会注意到它们不会加起来等于表格右下角的79.98。我计算它们加起来是79.93。
既然我知道有人会问,是的,79.98是正确的总数。应该加起来的值是不正确的。
我做错了什么?我已经调整了很长时间了,但没有看到任何变化。
编辑:
阅读一些评论后,很明显Math.Truncate()调用并没有帮助。以下是我之前的代码:
<tbody>
    @foreach (var day in user.WorkDays)
    {
        <tr>
            <th>@day.Date.ToString("MM/dd/yy")</th>
            <td>
                <ul>
                    @foreach (var note in day.Notes)
                    {
                        <li>@note.Text</li>
                    }
                </ul>
            </td>
            <td>@day.Totals.Regular.ToString("0.00")</td>
            <td>@day.Totals.Overtime.ToString("0.00")</td>
            <td>@day.Totals.Doubletime.ToString("0.00")</td>
            <td>@day.Totals.Sick.ToString("0.00")</td>
            <td>@day.Totals.Vacation.ToString("0.00")</td>
            <td>@day.Totals.Holiday.ToString("0.00")</td>
            <td>@day.Totals.Overall.ToString("0.00")</td>
        </tr>
    }
</tbody>
<tfoot>
    <tr>
        <th>Totals:</th>
        <th></th>
        <th>@user.Totals.Regular.ToString("0.00")</th>
        <th>@user.Totals.Overtime.ToString("0.00")</th>
        <th>@user.Totals.Doubletime.ToString("0.00")</th>
        <th>@user.Totals.Sick.ToString("0.00")</th>
        <th>@user.Totals.Vacation.ToString("0.00")</th>
        <th>@user.Totals.Holiday.ToString("0.00")</th>
        <th>@user.Totals.Overall.ToString("0.00")</th>
    </tr>
</tfoot>

现在,这些数值加起来甚至达到了80。即便如此,这个值仍然比实际值高了.02

1
评论中像“请帮帮我,Stack Overflow。你是我的唯一希望。”这样的话确实应该被删除。但是对于极客们的恳求,总有一种特殊的吸引力让我想点个赞。 - crthompson
3
哦,别这样。这一点也不会影响这篇文章的质量。 - keeehlan
3
如果你截断数字,我不明白你如何期望总数准确。 - Paolo Moretti
2
现实情况是底层数字无法准确显示为2位小数。考虑一下,如果您有:1.004, 1.004, 1.004但显示为1.00, 1.00, 1.00。您显示/手动计算的总和显然是3.00,但如果您对底层值求和,您将得到3.012(它将显示为3.01)。我不确定您在这里想要做什么。也许您应该将每行的值显示为更多的有效数字。 - Chris Sinclair
你有没有更改 SQL Server 列从 floatdecimal 的选项? - crthompson
显示剩余8条评论
3个回答

4
所以这里的真正问题是 - “总数”应该是“原始”的数字总和还是“截断”的数字总和。(作为一个旁注,ROUND会减少误差但不能消除它)。如果总数应该是原始数字的总和,则可以选择以下措施之一:
  1. 保留报告不变,并在脚注中解释由于四舍五入可能导致总数不完全匹配。
  2. 添加一个“四舍五入误差”的行。
  3. 将四舍五入误差分配给一个或多个项目(这里有一个关于如何做到这一点的问题和争论)。
如果总数应该是“四舍五入”的数字总和,则在报告中很容易实现,但它不会与原始数字的总和匹配(除非在源代码中对原始数字进行四舍五入)。

这是一组很好的建议。我会把它带给我们的财务人员和我的老板,以达成一个解决方案。 - keeehlan
由于你正在处理薪资单,选项3可能不是一个好选择。希望报告的使用者能够理解可能存在舍入误差。 - D Stanley
我认为他们会理解的。只需要解释清楚并确保他们明白正在发生的事情。 - keeehlan

3

您真的应该阅读这篇论文:

David Goldberg. 1991. 每个计算机科学家都应该了解浮点算术。 ACM计算机调查。23,1(1991年3月),5-48。 DOI = 10.1145 / 103162.103163 http://doi.acm.org/10.1145/103162.103163

摘要 很多人认为浮点运算是一门神秘的学科。 这相当令人惊讶,因为浮点在计算机系统中无处不在: 几乎每种语言都有一个浮点数类型;从个人��脑到 超级计算机都拥有浮点加速器;大多数编译器将被要求 不时地编译浮点算法;几乎每个操作系统 必须响应浮点异常,如溢出。本文介绍了关于浮点的方面 对计算机系统设计人员产生直接影响的教程。它开始于背景上浮点表示和舍入误差,然后讨论IEEE浮点标准,最后以计算机系统构建者更好地支持浮点的实例结束。

但问题很可能是您正在截断或舍入值以显示,并计算原始值的总和。 因此存在差异。

如果想使它们匹配:

  • 加总显示的值
  • 使用decimal而不是double

1
这不是浮点数错误问题。OP正在将组件截断为2位数字,并尝试将组件的总和与“截断”的数字的总和匹配。 - D Stanley

2

这里真正的问题并不在于 string.Format()。问题在于 Math.Truncate()。

让我来举个例子:

Math.Truncate(4.499999 * 100) / 100 = 4.49
Math.Round(4.499999, 2) = 4.50

现在0.01是一个相对较小的数目,但你的计算方式是导致问题的原因。你的例子有14行。如果每一行都是错误的,即使你的总数是正确的,如果人们自己计算,他们的显示结果也会是错误的。


我不确定那如何有所帮助。我不想要四舍五入,我需要一个精确的字符串表示,它是从 SQL Server 的 float 类型中获取的高精度 double 值。 - keeehlan
浮点数值并不是高度精确的:按设计,精度被换取为范围。 - Nicholas Carey
@kehrk,您想展示一个高精度的双精度数,但只需要2位小数。那么,Math.Round()方法应该能够满足您的需求。 - techvice
我会尝试一下。希望我只是误解了Math.Round()背后的逻辑,将其实践会让我更加明白。@techvice - keeehlan
实际上,不行。它导致我的值的总和加起来是 80 而不是正确的 79.98。@techvice - keeehlan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接