【问题标题】:Rounding to specified absolute decimal precision in C90/99在 C90/99 中舍入到指定的绝对十进制精度
【发布时间】:2013-05-15 07:31:37
【问题描述】:

我正在开发一种软​​件,该软件可以在文本和内部 (double) 表示之间转换测量的数字。该过程的一个必要部分是根据测量的统计不确定性生成具有正确小数精度的文本表示。所需的精度随数字而变化,其中的最低有效位可以是任何位置,包括(十进制)单位位置的左侧。

正确的舍入对于此过程至关重要,其中“正确”意味着根据当时有效的浮点舍入模式,或至少在明确定义的舍入模式下。因此,我需要小心(阅读:避免)对正在处理的数字执行中间算术,因为舍入甚至可能对数字内部表示中的最低有效位敏感。

如果我首先计算所需表示中的有效位数,我认为我可以使用 printf 系列函数相当好地完成几乎所有需要的格式化:

sprintf(buffer, "%.*e", num_sig_figs - 1, number);

到目前为止,有一类极端情况让我失望,但是:测量数字中的最高有效(十进制)数字位于所需精度表示的最低有效数字的右边一位。在这种情况下,四舍五入应在所需结果中产生最小(也是唯一)有效数字为 0 或 1,但我无法设计一种方法以可移植(*)方式执行舍入而不会有风险改变结果。这类似于 MPFR 函数mpfr_prec_round() 可以做的,除了它以二进制精度工作,而我需要使用十进制精度。

例如,在默认舍入模式下(从最接近的舍入到偶数舍入):

  • 0.5 表示为单位 (10^0) 精度应为“0”或“0e+00”
  • 654 表示为千 (10^3)​​ 精度应为“1e+03”
  • 0.03125 表示为十分之一 (10^-1) 精度应为“0”或“0e-01”甚至“0e+00”

(*) 这里的“可移植”是指代码准确地表达了标准的、可移植的 C99(或更好的 C90)中的计算。据了解,实际结果可能取决于机器细节,它应该取决于(并与)生效的浮点舍入模式。

我有什么选择?

【问题讨论】:

  • 十进制数和双精度数的舍入不能很好地结合在一起......(小数部分在二进制中是周期性的)尝试使用一些定点数据类型......
  • 看起来这些会用if (x < precision) if (x <= .5*precision) printf("0") else printf("%.0e", precision) else /* Not one of these cases, do other stuff. */…之类的代码来处理。是否存在您想要的与此代码产生的不同的情况? (precision 的舍入方式存在问题,因为 .1 无法准确表示,因此您可能在边界处遇到一些问题。此外,不处理负数 x,但这很简单。)跨度>
  • 请更好地解释您认为您的sprintf 方法有问题的情况。我认为这是正确的方法,我对您遇到的问题感到困惑。
  • sprintf 方法不适用于问题案例,因为 sprintf 将其结果基于参数的最高有效数字,(在这种情况下)不是所需结果的有效数字。例如,sprintf(buffer, "%.0e" 0.5) 产生“5e-01”而不是“0e+00”。只有在参数四舍五入的情况下,您才会得到非零结果。
  • 我意识到十进制舍入与二进制浮点不能很好地协调,并且大多数十进制分数都有无限的二进制表示。事实上,我的内部表示是一种有点笨拙的任意精度十进制格式。但是,我需要能够以标准浮点格式将数字接受到系统中,并且必须正确转换它们。

标签: c rounding


【解决方案1】:

一种始终有效的简单(尽管效率相当低)的方法是将完整的精确十进制值打印为字符串,然后手动以十进制进行四舍五入。这可以通过类似的方式来实现

snprintf(buf, sizeof buf, "%.*f", DBL_MANT_DIG-DBL_MIN_EXP, x);

我希望我得到了正确的精度。这个想法是,每增加一个尾数位,每增加一个 2 的负幂,就多占用一个小数位。

通过获得的十进制值是精确的这一事实,您避免了双舍入的问题。

请注意,双舍入仅在默认舍入模式(最近)中重要。在其他模式下,双舍入得到的结果与单舍入步骤的结果相同,因此您可以根据需要走很多捷径。

如果我想到它们,我可能会在稍后发布更好的解决方案。请注意,上述解决方案仅适用于 printf 系列函数能够打印精确小数的高质量实现。例如,在 MSVCRT 和其他低质量实现(甚至是一些符合标准的实现)上,它将严重失败。

【讨论】:

  • 嗯。我确实在内部使用十进制字符串,但效率低下,所以我并不羞于在这里依赖这种方法。我正在针对 glibc 2.5 进行开发,我认为这是一个不错的实现。我准备对不符合标准的实现嗤之以鼻,但令人担忧的是,符合标准的实现可能仍然会出错。不过,我会看看我是否可以在这些方面得到一些工作。敬请期待。
  • 如果您不喜欢依赖实现的printf 实现来支持精确的十进制打印,您可以编写自己的。所需要的只是一个非常基本的 bignum 以 10 的幂为基数(以 1000000000 为基数很方便),只需两个操作:乘法和除以 2 的幂。我所知道的最简洁(但有些神秘)的实现是我的:git.musl-libc.org/cgit/musl/tree/src/stdio/vfprintf.c(参见fmt_fp 函数)。
  • DBL_MANT_DIG-DBL_MIN_EXP 似乎是正确的想法,但0.5 需要四个字符以十进制表示,包括终止的零,并且它仅占“尾数位或二的负幂”的 1。最小安全值大概是DBL_MANT_DIG-DBL_MIN_EXP+3
  • @PascalCuoq:而0.5%.*f 的精确感中只占一个位置。 DBL_MANT_DIG-DBL_MIN_EXP 不是缓冲区大小而是精度。您的缓冲区需要相当大:足够大以容纳精度、小数点和整数部分(可能高达大约DBL_MAX_EXP/log2(10) 位)。
  • DBL_MANT_DIG 是尾数的 base-FLT_RADIX 位数,所以它不是很正确(除非你有十进制 FP 实现)。虽然它确实与 DBL_MIN_EXP 一起使用,它是一个基本的 FLT_RADIX 指数。在 C99 中,您可以使用 sprintf(buffer, "%.*e", DECIMAL_DIG - 1, number),只要缓冲区可以容纳 DECIMAL_DIG 数字以及包括前导符号、小数点和指数在内的修饰符。 C90 不需要定义 DECIMAL_DIG,但可以计算为ceil(1 + DBL_MANT_DIG * log10(FLT_RADIX))
猜你喜欢
  • 2016-12-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-03
  • 2010-12-08
  • 1970-01-01
相关资源
最近更新 更多