4

我想在 Java 和 C 中将 float/double 转换为字符串,这样输出既一致用户友好

通过“用户友好”,我的意思是字符串应该是人类可读和健全的:有效数字的最大数量,以及在适当时自动切换到科学记数法(双精度可以跨越所有有效范围)。

“一致”是指字符串在 Java 和 C 中应该完全相同(如果它们真的很少见,我会容忍一些例外)。

为什么不简单地使用一些printf格式字符串,如"%.5g"?那行得通……几乎。但遗憾的是,精度字段的含义在 Java 和 C 中完全不同。此外,从科学记数法到科学记数法的切换也不是很一致,甚至格式本身也不是很一致(指数为 2 或 3 位数字......)。并且不同的 C 编译器有时会产生不同的结果。

差异示例"%.5g"

double                  Java %.5g         gcc %.5g      tcc %.5g
1234.0                  1234.0            1234          1234 
123.45678               123.46            123.45678     123.46
0.000123456             0.00012346        0.00012346    0.00012346
0.000000000000123456    1.2346e-13        1.2346e-13    1.2346e-013

我可以用 C 或 Java(或两者)编写函数,但我想知道是否有人已经处理过这个问题。我不太关心性能,但是跨 C 编译器的可移植性是可以的。

4

4 回答 4

7

如果你真的想要以 10 为底的浮点输出,那么在printf这里为 C 编写一个 JNI 包装器可能是最简单的。Java 人决定他们需要自己做printf。除了你已经注意到的%g,他们决定以一种奇怪的方式改变舍入行为并截断输出。以机智:

System.out.printf("%.5g\n", 1.03125);
System.out.printf("%.5g\n", 1.09375);
1.0313
1.0938

gcc正确四舍五入:

printf("%.5g\n", 1.03125);
printf("%.5g\n", 1.09375);
1.0312
1.0938

请注意,1.03125 和 1.09375 可以精确地表示为双精度数,因为 1/32 = 0.3125。

Java 的 printf %g 格式错误地截断了它的输出:

double d = 1;
for (int i = 0; i < 1035; i++) d /= 2;
System.out.printf("%.20g\n%.20a\n", d, d);
2.7161546124360000000e-312
0x0.00080000000000000000p-1022

这是正确的答案:

double d = 1;
for (int i = 0; i < 1035; i++) d /= 2;
printf("%.20g\n%.20a\n", d, d);
2.7161546124355485633e-312
0x0.00080000000000000000p-1022

1.0e-200是正常的,但不能完全表示。Java 假装没有注意到:

System.out.printf("%.20g\n%.20a\n", 1.0e-200, 1.0e-200);
1.0000000000000000000e-200
0x1.87e92154ef7ac0000000p-665

这是正确的答案:

printf("%.20g\n%.20a\n", 1.0e-200, 1.0e-200);
9.999999999999999821e-201
0x1.87e92154ef7ac0000000p-665

因此,您要么必须忍受 printf 中奇怪的舍入行为,要么必须背负gccglibc工作。我不建议您尝试自己打印出浮点数。或者您可以只使用%aAFAIK 在 Java 中运行良好。

于 2012-12-07T15:45:10.007 回答
5

好吧,我结束了编写自己的函数。在 double 的所有范围内使用 gcc 和 tcc 进行测试,给出完全相同的输出(除了极少数非常小的值,小于 1E-319)

我发布它以防有人发现它有用。

爪哇:

     /**
     * Returns a double with an adhoc formatting, compatible with its C counterpart
     * 
     * If the absolute value is not too small or too big (thresholdLow-thresholdHigh)
     * the floating format is used, elsewhere the scientific.
     * In addition 
     *  - trailing zeros in fractional part are removed
     *  - if the value (or mantisa) is integer, a trailing .0 is always included
     *  - the exponent in sci notation is two or three digits
     *  - positive and negative zero returns "0.0"
     *  - special vals: "NaN" "Infinite" "-Infinite"
     * 
     * Remember to set Locale.setDefault(Locale.US) in your program.
     * 
     * @param v double
     * @param formatFloat floating point format, suggested: "%.5f"
     * @param formatSci   scientific format, must use lowercase 'e' : "%.5e"   
     * @param thresholdLow 
     * @param thresholdHigh
     * @return formatted string
     */
    public static String sprintfDouble(double v, String formatFloat, String formatSci, double thresholdLow,
            double thresholdHigh) {
        if(v==0.0)
            return "0.0"; //dont care about negative zero 
        if(Double.isInfinite(v) || Double.isNaN(v))
            return String.format(formatFloat,v);

        boolean neg = false;
        if (v < 0) {
            v = -v;
            neg = true;
        }
        String e = "";
        String res;
        if (v > thresholdLow && v < thresholdHigh) {
            res = String.format(formatFloat, v);
        } else {
            res = String.format(formatSci, v);
            int sp = res.indexOf('e');
            e = res.substring(sp);
            res = res.substring(0, sp);
        }
        if (res.indexOf('.') < 0)
            res += "."; // add decimal point if not present
        res = res.replaceAll("0+$", ""); // trim trailing zeros 
        if (res.endsWith("."))
            res += "0"; // add traiing zero if nec
        res += e;
        if (neg)
            res = "-" + res;
        return res;
    }

    public static String sprintfDouble5(double v){
        return sprintfDouble(v, "%.5f","%.5e",0.01,1000000.0);
    }

C:

char * sprintfDouble(char *buf, double v, const char *floatFormat, const char *sciFormat, double thresholdLow, double thresholdHigh) {
    char *p;
    char *pd; /* pointer to '.' */
    char *pe; /* pd=, pe=pointer to 'e' (or null terminator) */
    char *buforig;
    int trimmed;
    if(v != v) { /* nan */
        sprintf(buf,"NaN");
        return buf;
    }
    if(v == v && (v - v) != 0.0) { /* infinity */
        sprintf(buf, v <  0 ? "-Infinity" :"Infinity");
    return buf;
    } 
    if(v==0) { /* positive or negative zero, dont distinguish*/
        sprintf(buf, "0.0");
    return buf;
    }
    buforig = buf;
    if(v <0) {
        v = -v;
        buf[0] = '-';
        buf++;
    }
    if( v > thresholdLow && v < thresholdHigh ) {
        sprintf(buf,floatFormat, v);
        pe = buf+strlen(buf);
        pd = (char *) strchr(buf,'.');
        if(pd == NULL) { /* no decimal point? add it */
            pd = pe;
            *pe++ = '.';
            *pe++ = '0';
            *pe = 0;
        }
    } else {
        sprintf(buf,sciFormat, v);
        pe  =  (char *)strchr(buf,'e');
        pd =   (char *)strchr(buf,'.');
        if(pd ==NULL) { /* no decimal point with scientific notation? rare but... */
            p= buf+ strlen(buf);
            while(p>=pe) {
                *p = *(p-2);
                p--;
            }
            pd = pe;
            *pe++ = '.';
            *pe++ = '0';
            *pe = 0;
        }
        /* three digits exponent with leading zero? trim it */
        if( (*(pe+2) == '0' ) && ( strlen(buf) - (pe-buf))==5) {
            *(pe+2)=*(pe+3);
            *(pe+3)=*(pe+4);
            *(pe+4)=*(pe+5);
        }
    } /* now trim trailing zeros  */
    trimmed = 0;
    p=pe-1;
    while(*p =='0' ) {
        p--;
        trimmed++;
    }
    if(*p=='.') {
        trimmed--;    // dont trim the zero after the decimal point
        p++;
    }
    if(trimmed>0) {
        p = pe;
        while(1) {
            *(p-trimmed) = *p;
            if(*p==0) break;
            p++;
        }
    }
    return buforig;
}

char * sprintfDouble5(char *buf,double v) {
    return sprintfDouble(buf, v, "%.5f", "%.5e", 0.01, 1000000.0);
}

测试代码。

爪哇

static void test() { 
    Locale.setDefault(Locale.US);
    double start = 1.0;
    double x=start;
    for(int i=0;i<367;i++) {
        System.out.println(sprintfDouble5(x));
        x*= -7.0;
    }
    x=start;
    for(int i=0;i<6;i++) {
        System.out.println(sprintfDouble5(x));
        x/= -5;
    }
    for(int i=0;i<200;i++) {
        System.out.println(sprintfDouble5(x));
        x/= -42.01;
    }
    x=Math.PI*0.0000001;
    for(int i=0;i<20;i++) {
        System.out.println(sprintfDouble5(x));
        x*=10;
    }
    System.out.println(sprintfDouble5(0.0));
    System.out.println(sprintfDouble5(-0.0));
    System.out.println(sprintfDouble5(0.0/0.0));
}

C:

void test1() { 
    char buf[64];
    double start,x;
    int i;
    start = 1.0;
    x = start;
    for(i=0;i<367;i++) {
        printf("%s\n",sprintfDouble5(buf,x));
        x *= -7.0;
    }
    x = start;
    for(i=0;i<6;i++) {
        printf("%s\n",sprintfDouble5(buf,x));
        x /= -5;
    }
    for(i=0;i<200;i++) {
        printf("%s\n",sprintfDouble5(buf,x));
        x/= -42.01;
    }
    x = atan(1.0) * 4 * 0.0000001; /* PI */
    for(i=0;i<20;i++) {
        printf("%s\n",sprintfDouble5(buf,x));
        x *= 10;
    }
    printf("%s\n",sprintfDouble5(buf,0.0));
    printf("%s\n",sprintfDouble5(buf,-0.0));
    printf("%s\n",sprintfDouble5(buf,0.0/0.0));
}
于 2012-12-06T22:01:32.057 回答
1

这段代码

#include <stdio.h>

int main() {

    double v;
    char format[] = "%.5g\n";

    v = 1234.0;
    printf(format, v);


    v = 123.45678;
    printf(format, v);

    v = 0.000123456;
    printf(format, v);

    v = 0.000000000000123456;
    printf(format, v);

}

给我

1234
123.46
0.00012346
1.2346e-13

和这段代码

public class App13749802 {

    /**
     * @param args
     */
    public static void main(String[] args) {

        double v;
        String format = "%.5g";

        v = 1234.0;
        System.out.println(String.format(format, v));

        v = 123.45678;
        System.out.println(String.format(format, v));

        v = 0.000123456;
        System.out.println(String.format(format, v));

        v = 0.000000000000123456;
        System.out.println(String.format(format, v));
    }

}

给我

1234,0
123,46
0,00012346
1,2346e-13

逗号是因为我的区域设置。所以只有一个区别。

于 2012-12-06T18:37:27.337 回答
0

我创建了一个小型库 Double2String 来解决这个特定问题:

https://github.com/coconut2015/double2string

于 2021-07-12T02:31:07.367 回答