在有限的16字节字符串

时间:2015-09-17 13:11:21

标签: c floating-point precision dicom

这是我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

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

  1. Serge Ballesta
  2. chux
  3. Mark Dickinson
  4. chux
  5. 我在这里看到的结果是:

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

    所以compute4.c导致最佳的精度(0.001287056579548422< 4.050031792674619),但三元组(×3)的总执行时间(使用在调试模式下只测试time命令)

5 个答案:

答案 0 :(得分:2)

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和大小-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);
    }
}

答案 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. 由于'-',任何负数都会打印与正数相同的精度,精度会降低1。

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

  4. '.'不需要。

  5. 使用sprintf()以外的任何内容来执行数学部分非常危险许多极端情况。给定各种舍入模式FLT_EVAL_METHOD等,将繁重的编码留给完善的函数。

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

  7. 由于删除了'.'而导致的指数缩放必须在sprintf()之后1到1)避免注入计算错误2)将double递减到低于其{1}}最小指数。

  8. -1.00000000049999e-200一样,最大相对误差小于2,000,000,000中的1个部分。平均相对误差约为50,000,000,000的1份。

  9. 14位精度,最高,与12345678901234e1之类的数字一样,所以从16-2位开始。

  10. 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位双精度数。不太可读,但必须进行权衡。