【问题标题】:How to shift a floating-point value to the nearest one that can be represented exactly in a specific number of decimal places?如何将浮点值移动到可以精确表示为特定小数位数的最接近的值?
【发布时间】:2014-11-26 21:03:13
【问题描述】:

在 C++ 中是否有一种算法允许我在给定类型 T 的浮点值 V(例如 double 或 float)的情况下,在给定方向(向上或向下)返回最接近 V 的值以小于或等于指定的小数位数 D 表示准确

例如,给定

T = double 
V = 670000.08267799998 
D = 6

对于方向 = 朝向 +inf,我希望结果为 670000.082678,对于方向 = 朝向 -inf,我希望结果为 670000.082677

这有点类似于 std::nexttoward(),但有一个限制,即“下一个”值需要使用最多 D 个小数位精确表示。

我考虑过一种简单的解决方案,包括分离小数部分并将其缩放 10^D、截断它,然后再次缩放 10^-D 并将其添加回整数部分,但我没有t 相信这保证了结果值将在基础类型中完全可表示。

我希望有一种方法可以正确地做到这一点,但到目前为止我一直找不到。


编辑:我认为我最初的解释没有正确传达我的要求。在@patricia-shanahan 的建议下,我将尝试描述我的更高层次的目标,然后在这种情况下重新表述问题。

在最高级别,我需要这个例程的原因是由于一些业务逻辑,其中我必须接受一个双精度值 K 和一个百分比 P,将其拆分为两个双精度组件 V1 和 V2,其中 V1 ~= P 百分比K 和 V1 + V2 ~= K。要注意的是,V1 在通过有线协议发送到第三方之前用于进一步的计算,该有线协议接受字符串格式的浮点值,最多 D 位小数。因为发送给第 3 方的值(字符串格式)需要与使用 V1(双格式)进行的计算结果相一致,所以我需要使用某些函数 F()“调整”V1,使其如下所示尽可能接近 K 的 P%,同时仍然可以使用最多 D 个小数位精确表示为字符串格式。 V2 没有 V1 的限制,可以计算为 V2 = K - F(V1)(可以理解和可以接受的是,这可能导致 V2 使得 V1 + V2 非常接近但不完全等于 K) .

在较低级别,我希望编写该例程来“调整”V1,并具有以下签名:

double F(double V, unsigned int D, bool roundUpIfTrueElseDown);

其中的输出是通过取 V 和(如有必要,按 bool 参数指定的方向)将其四舍五入到小数点后第 D 位来计算的。

我的期望是当 V 被如下序列化出来时

const auto maxD = std::numeric_limits<double>::digits10;
assert(D <= maxD); // D will be less than maxD... e.g. typically 1-6, definitely <= 13
std::cout << std::fixed 
          << std::setprecision(maxD) 
          << F(V, D, true);

那么输出仅包含第 D 个小数位以外的零。

请务必注意,出于性能原因,我正在寻找不涉及在双精度和字符串格式之间来回转换的 F() 实现。虽然输出最终可能会转换为字符串格式,但在许多情况下,逻辑会在必要之前提前输出,我希望避免这种情况下的开销。

【问题讨论】:

  • 在您的示例中,两个结果相差 0.000001,这不能完全表示为二进制浮点数,因此至少其中一个也不能完全表示。
  • 我的意思是,其中至少有一个是可表示的。
  • 更进一步,考虑到这样一个特性的行为显然不是你所期望的,它是否有一个有用的目的是值得商榷的。
  • @ChrisKline 你能把问题描述上一层吗?你会用它做什么?最好的解决方案可能是使用十进制类型。可能是接受一个双精度数,当四舍五入到 D 位时,它会显示为您想要的值。
  • D 似乎是小数点之后的位数,而不是总数。您确定要 浮点 点吗?定点算术呢?

标签: c++ c floating-point decimal precision


【解决方案1】:

完全重写。

根据 OP 的新要求并使用@Patricia Shanahan 建议的 2 次幂,简单的 C 解决方案:

double roundedV = ldexp(round(ldexp(V, D)),-D);  // for nearest
double roundedV = ldexp(ceil (ldexp(V, D)),-D);  // at or just greater
double roundedV = ldexp(floor(ldexp(V, D)),-D);  // at or just less

除了@Patricia Shanahan之外,这里唯一添加的就是匹配OP标签的C代码。

【讨论】:

    【解决方案2】:

    一般来说,十进制分数不能精确地表示为二进制分数。有一些例外,例如 0.5 (½) 和 16.375 (16⅜),因为所有二进制分数都可以精确地表示为十进制分数。 (这是因为 2 是 10 的因数,但 10 不是 2 的因数,也不是 2 的任何幂。)但如果一个数不是 2 的某个幂的倍数,它的二进制表示将是一个无限长的循环序列,就像十进制中 ⅓ 的表示 (.333....)。

    标准C库提供宏DBL_DIG(一般为15);任何具有那么多小数位精度的十进制数都可以转换为double(例如,scanf),然后再转换回十进制表示(例如,printf)。要在不丢失信息的情况下朝相反的方向前进——从double 开始,将其转换为十进制,然后再转换回来——您需要 17 个十进制数字 (DBL_DECIMAL_DIG)。 (我引用的值基于 IEEE-754 64 位双精度)。

    提供接近问题的一种方法是将精度不超过 DBL_DIG 位的十进制数视为浮点数的“精确但不精确”表示,如果该浮点数是最接近十进制数的值的浮点数。找到该浮点数的一种方法是使用scanfstrtod 将十进制数转换为浮点数,然后尝试附近的浮点数(使用nextafter 探索)找到哪些转换为具有DBL_DIG 精度数字的相同表示。

    如果您相信标准库实现不会太远,您可以使用 sprintf 将您的 double 转换为十进制数,在所需的数字位置增加十进制字符串(这只是一个字符串操作) , 然后用strtod 将其转换回double

    【讨论】:

    • @chux:好的,我会解决的。虽然现在我不知道我为什么要麻烦包含它。
    【解决方案3】:

    在 C++ 中,整数必须用二进制表示,但浮点类型可以有十进制表示。

    如果 &lt;limits.h&gt; 中的 FLT_RADIX 是 10 或 10 的某个倍数,那么您可以实现精确表示十进制值的目标。

    否则,一般情况下是无法实现的。

    所以,第一步,尝试找到一个 C++ 实现,其中FLT_RADIX 是 10。

    在安装 C++ 实现并证明可以在您的系统上运行之前,我不会担心算法或其效率。但作为提示,您的目标似乎与被称为“舍入”的操作非常相似。我想,在获得我的十进制浮点 C++ 实现之后,我会开始研究四舍五入的技术,例如,谷歌搜索,也许是 Wikipedia,......

    【讨论】:

      【解决方案4】:

      这是执行所要求的程序的草图。它主要是为了找出这是否真的是想要的。我用 Java 编写它,因为该语言对我想要依赖的浮点运算有一些保证。我只使用BigDecimal 来精确显示双精度数,以表明答案完全可以表示,小数点后不超过 D 位。

      具体来说,我依赖于根据 IEEE 754 64 位二进制算术的双重行为。对于 C++,这是可能的,但标准不能保证。我还依赖于 Math.pow 对于简单精确情况的精确性、除以 2 的幂的精确性以及能够使用 BigDecimal 获得精确输出。

      我没有处理过极端情况。最大的缺失部分是处理具有大 D 的大数。我假设括号内的二进制分数完全可以表示为双精度数。如果它们的有效位超过 53 个,则情况并非如此。它还需要代码来处理无穷大和 NaN。对于次正规数,用 2 的幂除的正确性假设是不正确的。如果您需要您的代码来处理它们,则必须进行更正。

      它基于这样的概念,即一个数字既可以精确表示为小数点后不超过 D 位的小数,又可以精确表示为二进制分数,必须可以表示为分母 2 提升到D 电源。如果分母需要更高的 2 次方,则十进制形式的小数点后需要多于 D 位。如果它根本不能表示为分母为 2 次方的分数,则它不能完全表示为双精度数。

      虽然我跑了一些其他案例来说明,但关键输出是:

      670000.082678 to 6 digits Up: 670000.09375 Down: 670000.078125

      这是程序:

      import java.math.BigDecimal;
      
      public class Test {
        public static void main(String args[]) {
          testIt(2, 0.000001);
          testIt(10, 0.000001);
          testIt(6, 670000.08267799998);
        }
      
        private static void testIt(int d, double in) {
          System.out.print(in + " to " + d + " digits");
          System.out.print(" Up: " + new BigDecimal(roundUpExact(d, in)).toString());
          System.out.println(" Down: "
              + new BigDecimal(roundDownExact(d, in)).toString());
        }
      
        public static double roundUpExact(int d, double in) {
          double factor = Math.pow(2, d);
          double roundee = factor * in;
          roundee = Math.ceil(roundee);
          return roundee / factor;
        }
      
        public static double roundDownExact(int d, double in) {
          double factor = Math.pow(2, d);
          double roundee = factor * in;
          roundee = Math.floor(roundee);
          return roundee / factor;
        }
      }
      

      【讨论】:

      • 谢谢帕特里夏!您介意告诉我您想依赖 Java 中的哪些 FP 算术保证吗?我想在 C++ 中尝试一下;也许通过配置各种 FP 控制标志可以实现一些相同的保证。
      • @ChrisKline 我在算术依赖和边缘情况方面编辑了更多细节。
      • 感谢您提供的其他信息,这非常有帮助。我相信我可以捕获边缘情况并适当地处理它们。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-18
      • 1970-01-01
      • 1970-01-01
      • 2022-11-17
      • 2015-11-12
      • 2010-12-26
      相关资源
      最近更新 更多