这是我original post的后续行动。但为了清楚起见,我会重复一遍:
根据DICOM标准,可以使用十进制字符串的值表示来存储一种浮点。见Table 6.2-1. DICOM Value Representations:
十进制字符串:表示固定字符的字符串 点号或浮点数。固定点数应 仅包含字符0-9,带有可选的前导“+”或“ - ” 和一个可选的“。”标记小数点。浮点数 应按照ANSI X3.9的规定进行传送,并带有“E”或“e” 表示指数的开始。十进制字符串可以填充 前导或尾随空格。不允许使用嵌入式空间。
“0” - “9”,“+”,“ - ”,“E”,“e”,“。”和默认的SPACE字符 角色保留曲目。最多16个字节
标准是说文本表示是固定点与浮点。该标准仅涉及如何在DICOM数据集本身中表示值。因此,不需要将定点文本表示加载到定点变量中。
现在很清楚,DICOM标准明确建议double
(IEEE 754-1985)表示Value Representation
类型Decimal String
(最多16位有效数字)。我的问题是如何使用标准的C I / O库将这个二进制表示从内存转换回ASCII到这个有限大小的字符串?
从互联网上的随机来源来看,这是非常重要的,但通常accepted solution是:
printf("%1.16e\n", d); // Round-trippable double, always with an exponent
或
printf("%.17g\n", d); // Round-trippable double, shortest possible
当然,在我的情况下,这两个表达式都是无效的,因为它们可以产生比我有限的 16字节的最大值更长的输出。那么当将一个任意的double值写入有限的16字节字符串时,最小化精度损失的解决方案是什么?
修改:如果不清楚,我需要遵循标准。我不能使用hex / uuencode编码。
编辑2 :我正在使用travis-ci运行比较,请参阅:here
到目前为止,建议的代码是:
我在这里看到的结果是:
compute1.c
导致总和误差为:0.0095729050923877828
compute2.c
导致总和误差为:0.21764383725715469
compute3.c
导致总和误差为:4.050031792674619
compute4.c
导致总和误差为:0.001287056579548422
所以compute4.c
导致最佳的精度(0.001287056579548422< 4.050031792674619),但三元组(×3)的总执行时间(使用在调试模式下只测试time
命令)
答案 0 :(得分:2)
C库格式化程序没有直接格式符合您的要求。在简单的层面上,如果您接受标准%g
格式的字符的浪费(e20
写入e+020
:2个字符浪费),您可以:
%.17g
格式代码可能如下所示:
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)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);
}
}
答案 1 :(得分:2)
对于有限浮点值,printf()
格式说明符"%e"
很好地匹配
“浮点数应为......带”E“或”e“表示指数的开始”
[−]d.ddd...ddde±dd
该标志带有负数,可能为-0.0
。指数至少为2位数。
如果我们假设 DBL_MAX < 1e1000
,(对IEEE 754-1985 double安全),则以下适用于所有情况: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 width specifier to maintain precision of floating-point value
候选人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);
答案 2 :(得分:2)
比起初想的更棘手。
鉴于各种角落情况,最好以高精度尝试,然后根据需要进行处理。
由于'-'
,任何负数都会打印与正数相同的精度,精度会降低1。
'+'
之后不需要 'e'
符号。
'.'
不需要。
使用sprintf()
以外的任何内容来执行数学部分非常危险许多极端情况。给定各种舍入模式FLT_EVAL_METHOD
等,将繁重的编码留给完善的函数。
当尝试次数超过1个字符时,可以保存迭代次数。例如。如果精度为14的尝试宽度为20,则无需尝试精度13和12,只需转到11。
由于删除了'.'
而导致的指数缩放必须在sprintf()
之后1到1)避免注入计算错误2)将double
递减到低于其{1}}最小指数。
与-1.00000000049999e-200
一样,最大相对误差小于2,000,000,000中的1个部分。平均相对误差约为50,000,000,000的1份。
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(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());
}
输出
-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'
答案 3 :(得分:1)
我认为你最好的选择是使用printf(“%。17g \ n”,d);生成初始答案,然后修剪它。修剪它的最简单方法是从尾数末尾删除数字,直到它适合。这实际上非常有效但不会最小化错误,因为您正在截断而不是舍入到最近。
更好的解决方案是检查要删除的数字,将它们视为介于0.0和1.0之间的n位数字,因此'49'将为0.49。如果它们的值小于0.5,那么只需将它们删除即可。如果它们的值大于0.50,则以十进制形式递增打印值。也就是说,在最后一位数字中添加一个,并根据需要进行环绕和携带。应修剪任何创建的尾随零。
这成为问题的唯一时间是进位一直传播到第一个数字并从9溢出到零。这可能是不可能的,但我不确定。在这种情况下(+ 9.99999e17)答案是+ 1e18,所以只要你对这种情况进行测试就应该没问题。
因此,打印数字,将其拆分为符号/尾数字符串和指数整数,并对字符串进行操作以获得结果。
答案 4 :(得分:0)
以十进制打印不起作用,因为对于某些数字,需要一个17位数的尾数,这会消耗所有空间而不打印指数。更准确地说,以十进制打印双倍有时需要超过16个字符以保证准确的往返。
相反,您应该使用十六进制打印基础二进制表示。假设不需要空终止符,这将使用恰好16个字节。
如果您想使用少于16个字节打印结果,那么您基本上可以对其进行编码。也就是说,使用超过16位数字,以便您可以在每个数字中挤出更多位。如果使用64个不同的字符(6位),则可以用11个字符打印64位双精度数。不太可读,但必须进行权衡。