在有限的 16 字节字符串上写入 IEEE 754-1985 双作为 ASCII

mal*_*lat 5 c floating-point precision

这是我原帖的后续。但为了清楚起见,我会重复一遍:

根据 DICOM 标准,可以使用十进制字符串的值表示来存储一种类型的浮点。请参阅表6.2-1。DICOM 值表示

十进制字符串:表示定点数或浮点数的字符串。定点数应仅包含字符 0-9 以及可选的前导“+”或“-”和可选的“.”。来标记小数点。浮点数应按照 ANSI X3.9 中的定义传送,用“E”或“e”表示指数的开始。十进制字符串可以用前导或尾随空格填充。不允许嵌入空格。

“0”-“9”、“+”、“-”、“E”、“e”、“.” 和默认字符曲目的空格字符。最大 16 字节

标准是说文本表示是定点与浮点。该标准仅涉及值在 DICOM 数据集本身中的表示方式。因此,不需要将定点文本表示加载到定点变量中。

所以现在很明显,DICOM 标准隐含地推荐double(IEEE 754-1985) 来表示Value Representation类型Decimal String(最多 16 位有效数字)。我的问题是如何使用标准 CI/O 库将这个二进制表示从内存转换回 ASCII 到这个有限大小的字符串?

从互联网上的随机来源来看,这并非易事,但普遍接受的解决方案是:

printf("%1.16e\n", d); // Round-trippable double, always with an exponent
Run Code Online (Sandbox Code Playgroud)

或者

printf("%.17g\n", d); // Round-trippable double, shortest possible
Run Code Online (Sandbox Code Playgroud)

当然,这两个表达式在我的情况下都是无效的,因为它们可以产生比我限制的最大16 字节长得多的输出。那么将任意双精度值写入有限的 16 字节字符串时,最小化精度损失的解决方案是什么?


编辑:如果这不清楚,我需要遵循标准。我不能使用十六进制/uuencode 编码。

编辑 2:我正在使用 travis-ci 进行比较,请参阅:here

到目前为止,建议的代码是:

  1. 塞尔吉·巴列斯塔
  2. 丘克斯
  3. 马克·狄金森
  4. 丘克斯

我在这里看到的结果是:

  • compute1.c 导致总和误差为: 0.0095729050923877828
  • compute2.c 导致总和误差为: 0.21764383725715469
  • compute3.c 导致总和误差为: 4.050031792674619
  • compute4.c 导致总和误差为: 0.001287056579548422

因此compute4.c导致最佳精度(0.001287056579548422 < 4.050031792674619),但总执行时间增加了三倍(x3)(仅在调试模式下使用time命令测试)。

chu*_*ica 3

这比最初想象的要棘手。

考虑到各种极端情况,似乎最好尝试高精度,然后根据需要进行降低。

  1. 任何负数的打印结果与正数相同,但由于'-'.

  2. '+'字符串开头或之后都不需要符号'e'

  3. '.'不需要。

  4. 考虑到如此多的sprintf()极端情况,除了数学部分之外,使用任何其他方法都是危险的。给定各种舍入模式等,将繁重的编码留给完善的函数。FLT_EVAL_METHOD

  5. 当一次尝试太长超过 1 个字符时,可以保存迭代。例如,如果尝试使用精度 14,结果宽度为 20,则无需尝试精度 13 和 12,只需转到 11。

  6. 由于删除 而导致的指数缩放,必须在 1) 避免注入计算错误 2) 将 a 递减至其最小指数以下之后'.'进行。sprintf()double

  7. 最大相对误差小于 2,000,000,000 分之一-1.00000000049999e-200。平均相对误差约为50,000,000,000分之一。

  8. 最高的 14 位精度出现在以12345678901234e116-2 位开头的数字中。


static size_t shrink(char *fp_buffer) {
  int lead, expo;
  long long mant;
  int n0, n1;
  int n = sscanf(fp_buffer, "%d.%n%lld%ne%d", &lead, &n0, &mant, &n1, &expo);
  assert(n == 3);
  return sprintf(fp_buffer, "%d%0*llde%d", lead, n1 - n0, mant,
          expo - (n1 - n0));
}

int x16printf(char *dest, size_t width, double value) {
  if (!isfinite(value)) return 1;

  if (width < 5) return 2;
  if (signbit(value)) {
    value = -value;
    strcpy(dest++, "-");
    width--;
  }
  int precision = width - 2;
  while (precision > 0) {
    char buffer[width + 10];
    // %.*e prints 1 digit, '.' and then `precision - 1` digits
    snprintf(buffer, sizeof buffer, "%.*e", precision - 1, value);
    size_t n = shrink(buffer);
    if (n <= width) {
      strcpy(dest, buffer);
      return 0;
    }
    if (n > width + 1) precision -= n - width - 1;
    else precision--;
  }
  return 3;
}
Run Code Online (Sandbox Code Playgroud)

测试代码

double rand_double(void) {
  union {
    double d;
    unsigned char uc[sizeof(double)];
  } u;
  do {
    for (size_t i = 0; i < sizeof(double); i++) {
      u.uc[i] = rand();
    }
  } while (!isfinite(u.d));
  return u.d;
}

void x16printf_test(double value) {
  printf("%-27.*e", 17, value);
  char buf[16+1];
  buf[0] = 0;
  int y = x16printf(buf, sizeof buf - 1, value);
  printf(" %d\n", y);
  printf("'%s'\n", buf);
}


int main(void) {
  for (int i = 0; i < 10; i++)
    x16printf_test(rand_double());
}
Run Code Online (Sandbox Code Playgroud)

输出

-1.55736829786841915e+118   0
'-15573682979e108'
-3.06117209691283956e+125   0
'-30611720969e115'
8.05005611774356367e+175    0
'805005611774e164'
-1.06083057094522472e+132   0
'-10608305709e122'
3.39265065244054607e-209    0
'33926506524e-219'
-2.36818580315246204e-244   0
'-2368185803e-253'
7.91188576978592497e+301    0
'791188576979e290'
-1.40513111051994779e-53    0
'-14051311105e-63'
-1.37897140950449389e-14    0
'-13789714095e-24'
-2.15869805640288206e+125   0
'-21586980564e115'
Run Code Online (Sandbox Code Playgroud)