如何将浮点值移到可以精确表示为特定小数位数的最近值

How to shift a floating-point value to the nearest one that can be represented exactly in a specific number of decimal places?

本文关键字:表示 小数 最近      更新时间:2023-10-16

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

例如,给定

T = double 
V = 670000.08267799998 
D = 6

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

这与std::nexttoward()有点相似,但有一个限制,即"next"值最多需要使用小数点后D位才能精确表示。

我曾考虑过一个简单的解决方案,包括将分数部分分离出来,按10^D缩放,截断它,再按10^-D缩放,然后将它重新固定到整数部分,但我不相信这能保证得到的值在底层类型中准确表示。

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


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

在最高级别上,我需要这个例程的原因是由于一些业务逻辑,其中我必须接受一个双值K和一个百分比p,将其拆分为两个双分量V1和V2,其中V1~=p%的K和V1+V2~=K。问题是V1在通过有线协议发送给第三方之前被用于进一步的计算,该协议接受最多小数点后D位的字符串格式的浮点值。因为发送给第三方的值(字符串格式)需要与使用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并(如有必要,按照布尔参数指定的方向)将其四舍五入到小数点后第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位以外的零。

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

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

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

我没有处理过边缘案件。最大的遗漏是处理具有大D的大星等。我假设括号内的二进制分数可以精确地表示为二重。如果它们具有超过53个有效位,则情况并非如此。它还需要处理不定式和NaN的代码。二次幂除法的精确性的假设对于次正规数是不正确的。如果您需要您的代码来处理它们,则必须进行更正。

它基于这样一个概念,即一个既可以精确地表示为小数点后不超过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;
  }
}

通常,十进制分数不能精确地表示为二进制分数。也有一些例外,比如0.5(1/2)和16.375(16/),因为所有二进制分数都可以精确地表示为十进制分数。(这是因为2是10的因子,但10不是2的因子,也不是2的任何幂。)但如果一个数字不是2的某个幂的倍数,它的二进制表示将是一个无限长的循环序列,就像1/3的表示一样;十进制(.333….)。

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

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

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

总重写。

根据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代码。

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

如果<limits.h>中的FLT_RADIX是10,或者是10的倍数,则可以实现精确表示十进制值的目标。

否则,总的来说,这是不可能实现的。

因此,作为第一步,尝试找到一个FLT_RADIX为10的C++实现。

我不会担心算法或其效率,直到C++实现被安装并被证明在您的系统上工作。但作为一个暗示,你的目标似乎与所谓的“四舍五入";。我认为,在获得我的十进制浮点C++实现后,我;d首先研究四舍五入的技术,例如,在谷歌上搜索,也许是维基百科,…