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

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

根据DICOM标准,可以使用十进制字符串的值表示来存储一种浮点。 见表6.2-1。 DICOM价值表示 :

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

“0” – “9”,“+”,“ – ”,“E”,“e”,“。” 和默认字符保留曲目的SPACE字符。 最多16个字节

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

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

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

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

要么

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

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


编辑 :如果不清楚,我需要遵循标准。 我不能使用hex / uuencode编码。

编辑2 :我正在使用travis-ci运行比较,请参阅: 此处

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

  1. Serge Ballesta
  2. chux
  3. 马克迪金森
  4. chux

我在这里看到的结果是:

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

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

它比最初的想法更棘手。

鉴于各种角落情况,最好以高精度尝试,然后根据需要进行处理。

  1. 由于'-'任何负数都会打印相同的正数,精度会降低1。

  2. 在字符串的开头和'e'之后不需要'+'符号。

  3. '.' 不需要。

  4. 除了sprintf()之外的任何东西sprintf()危险,在这么极端情况下做数学部分。 给定各种舍入模式, FLT_EVAL_METHOD等,将繁重的编码留给完善的函数。

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

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

  7. -1.00000000049999e-200相比,最大相对误差小于-1.00000000049999e-200 1份。 平均相对误差约为50,000,000,000的1份。

  8. 最高的14位精度出现在12345678901234e1这样的数字上,所以从16-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; } 

测试代码

 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(ud)); return ud; } 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()); } 

产量

 -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' 

对于有限浮点值, printf()格式说明符"%e"匹配良好
“浮点数应为……带”E“或”e“表示指数的开始”

 [−]d.ddd...ddde±dd 

符号以负数显示,可能为-0.0 。 指数至少为2位数。

如果我们假设 DBL_MAX < 1e1000 ,(对于IEEE 754-1985双重安全),则以下适用于所有情况:1个可选符号,1个引导数字, '.' ,8位数, 'e' ,符号,最多3位数。

(注意:“16字节最大值”似乎不是指C字符串空字符终止。如果需要,请调整1。)

 // Room for 16 printable characters. char buf[16+1]; int n = snprintf(buf, sizeof buf, "%.*e", 8, x); assert(n >= 0 && n < sizeof buf); puts(buf); 

但是这为可选符号和2到3个指数数字保留了空间。

诀窍是边界,由于四舍五入,当数字使用2或使用3个​​指数数字时是模糊的。 即使测试负数, -0.0也是一个问题。

[编辑]也需要测试非常小的数字。

候选人:

 // Room for 16 printable characters. char buf[16+1]; assert(isfinite(x)); // for now, only address finite numbers int precision = 8+1+1; if (signbit(x)) precision--; // Or simply `if (x <= 0.0) precision--;` if (fabs(x) >= 9.99999999e99) precision--; // some refinement possible here. else if (fabs(x) <= 1.0e-99) precision--; int n = snprintf(buf, sizeof buf, "%.*e", precision, x); assert(n >= 0 && n < sizeof buf); puts(buf); 

其他问题:

一些编译器打印至少3个指数位。
IEEE 754-1985 double 所需的最大十进制有效数字的数量因需要的定义而异,但可能大约为15-17。 Printf宽度说明符,用于保持浮点值的精度

候选人2:一次测试输出太长时间

 // Room for N printable characters. #define N 16 char buf[N+1]; assert(isfinite(x)); // for now, only address finite numbers int precision = N - 2 - 4; // 1.xxxxxxxxxxe-dd if (signbit(x)) precision--; int n = snprintf(buf, sizeof buf, "%.*e", precision, x); if (n >= sizeof buf) { n = snprintf(buf, sizeof buf, "%.*e", precision - (n - sizeof buf) - 1, x); } assert(n >= 0 && n < sizeof buf); puts(buf); 

C库格式化程序没有直接格式符合您的要求。 在一个简单的层面上,如果您可以接受标准%g格式的字符浪费( e20写入e+020 :2字符浪费),您可以:

  • 生成%.17g格式的输出
  • 如果它大于16个字符,则计算导致16的精度
  • 生成该格式的输出。

代码可能如下所示:

 void encode(double f, char *buf) { char line[40]; char format[8]; int prec; int l; l = sprintf(line, "%.17g", f); if (l > 16) { prec = 33 - strlen(line); l = sprintf(line, "%.*g", prec, f); while(l > 16) { /* putc('.', stdout);*/ prec -=1; l = sprintf(line, "%.*g", prec, f); } } strcpy(buf, line); } 

如果你真的想要达到最优(意味着写e30而不是e + 030),你可以尝试使用%1.16e格式并对输出进行后处理。 理由(正数):

  • %1.16e格式允许您分隔尾数和指数(基数10)
  • 如果exponenent介于size-2(包含)和size(排除)之间:只需将尾数正确地舍入到int部分并显示它
  • 如果指数介于0和size-2之间(均包括在内):显示正确放置点的圆形尾数
  • 如果指数介于-1和-3之间(均包括在内):以点开头,添加最终0并用圆形尾数填充
  • 否则使用指数部分最小尺寸的e格式,并用圆形尾数填充

拐角案件:

  • 对于负数,放一个开始-并添加相反数字和大小-1的显示
  • 舍入:如果第一个被拒绝的数字>=5 ,则增加前面的数字,如果是9则迭代。 处理9.9999999999...作为特殊情况舍入到10

可能的代码:

 void clean(char *mant) { char *ix = mant + strlen(mant) - 1; while(('0' == *ix) && (ix > mant)) { *ix-- = '\0'; } if ('.' == *ix) { *ix = '\0'; } } int add1(char *buf, int n) { if (n < 0) return 1; if (buf[n] == '9') { buf[n] = '0'; return add1(buf, n-1); } else { buf[n] += 1; } return 0; } int doround(char *buf, unsigned int n) { char c; if (n >= strlen(buf)) return 0; c = buf[n]; buf[n] = 0; if ((c >= '5') && (c <= '9')) return add1(buf, n-1); return 0; } int roundat(char *buf, unsigned int i, int iexp) { if (doround(buf, i) != 0) { iexp += 1; switch(iexp) { case -2: strcpy(buf, ".01"); break; case -1: strcpy(buf, ".1"); break; case 0: strcpy(buf, "1."); break; case 1: strcpy(buf, "10"); break; case 2: strcpy(buf, "100"); break; default: sprintf(buf, "1e%d", iexp); } return 1; } return 0; } void encode(double f, char *buf, int size) { char line[40]; char *mant = line + 1; int iexp, lexp, i; char exp[6]; if (f < 0) { f = -f; size -= 1; *buf++ = '-'; } sprintf(line, "%1.16e", f); if (line[0] == '-') { f = -f; size -= 1; *buf++ = '-'; sprintf(line, "%1.16e", f); } *mant = line[0]; i = strcspn(mant, "eE"); mant[i] = '\0'; iexp = strtol(mant + i + 1, NULL, 10); lexp = sprintf(exp, "e%d", iexp); if ((iexp >= size) || (iexp < -3)) { i = roundat(mant, size - 1 -lexp, iexp); if(i == 1) { strcpy(buf, mant); return; } buf[0] = mant[0]; buf[1] = '.'; strncpy(buf + i + 2, mant + 1, size - 2 - lexp); buf[size-lexp] = 0; clean(buf); strcat(buf, exp); } else if (iexp >= size - 2) { roundat(mant, iexp + 1, iexp); strcpy(buf, mant); } else if (iexp >= 0) { i = roundat(mant, size - 1, iexp); if (i == 1) { strcpy(buf, mant); return; } strncpy(buf, mant, iexp + 1); buf[iexp + 1] = '.'; strncpy(buf + iexp + 2, mant + iexp + 1, size - iexp - 1); buf[size] = 0; clean(buf); } else { int j; i = roundat(mant, size + 1 + iexp, iexp); if (i == 1) { strcpy(buf, mant); return; } buf[0] = '.'; for(j=0; j< -1 - iexp; j++) { buf[j+1] = '0'; } if ((i == 1) && (iexp != -1)) { buf[-iexp] = '1'; buf++; } strncpy(buf - iexp, mant, size + 1 + iexp); buf[size] = 0; clean(buf); } } 

我认为你最好的选择是使用printf(“%。17g \ n”,d); 生成初始答案,然后修剪它。 修剪它的最简单方法是从尾数末端删除数字直到它适合。 这实际上非常有效但不会最小化错误,因为您正在截断而不是舍入到最近。

一个更好的解决方案是检查要删除的数字,将它们视为介于0.0和1.0之间的n位数字,因此’49’将为0.49。 如果它们的值小于0.5,那么只需将它们删除即可。 如果它们的值大于0.50,则以十进制forms递增打印值。 也就是说,在最后一位数字中添加一个,并根据需要进行环绕和携带。 应修剪任何创建的尾随零。

这成为问题的唯一时间是进位一直传播到第一个数字并从9溢出到零。 这可能是不可能的,但我不确定。 在这种情况下(+ 9.99999e17)答案是+ 1e18,所以只要你对这种情况进行测试就应该没问题。

因此,打印数字,将其拆分为符号/尾数字符串和指数整数,并对字符串进行操作以获得结果。

以十进制打印不起作用,因为对于某些数字,需要一个17位的尾数,它会占用你所有的空间而不打印指数。 更准确地说,以十进制打印双倍有时需要超过16个字符以保证准确的往返。

相反,您应该使用hex打印基础二进制表示。 假设不需要空终止符,这将使用恰好16个字节。

如果您想使用少于16个字节打印结果,那么您基本上可以对其进行编码。 也就是说,使用超过16位数字,以便您可以在每个数字中挤出更多位。 如果使用64个不同的字符(6位),则可以以11个字符打印64位双精度。 不太可读,但必须进行权衡。