Skip to content
Advertisement

C compatible printf output for Java

I’d want to convert float/double to string, in Java and C, such that the outputs are both consistent and user friendly.

By “user friendly”, I mean the string should be human readable and sound: a maximum number of significant digits, and some automatic switching to scientific notation when appropiate (the double could span all the valid range).

By “consistent” I mean that the strings should be exactly the same in Java and C (I’d tolerate some exceptions if they are really rare).

Why not use simply some printf format string, as "%.5g"? That works… almost. But sadly the meaning of the precision field is quite different in Java and C. Also, the switching from-to scientific notation is not very consistent, nor even the format itself (2 or 3 digits for the exponent…). And different C compilers sometimes produce different results.

Examples of differences for "%.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

I can code a function in C or Java (or both), but I wonder if someone has already dealt with this. I’m not very concerned with performance, but yes with portability across C compilers.

Answer

Well, I ended coding my own functions. Tested with gcc and tcc over all the range of double, gives exactly the same output (except for very few very small values, less than 1E-319)

I post it in case someone finds it useful.

Java:

     /**
     * 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);
}

Test code.

Java

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("%sn",sprintfDouble5(buf,x));
        x *= -7.0;
    }
    x = start;
    for(i=0;i<6;i++) {
        printf("%sn",sprintfDouble5(buf,x));
        x /= -5;
    }
    for(i=0;i<200;i++) {
        printf("%sn",sprintfDouble5(buf,x));
        x/= -42.01;
    }
    x = atan(1.0) * 4 * 0.0000001; /* PI */
    for(i=0;i<20;i++) {
        printf("%sn",sprintfDouble5(buf,x));
        x *= 10;
    }
    printf("%sn",sprintfDouble5(buf,0.0));
    printf("%sn",sprintfDouble5(buf,-0.0));
    printf("%sn",sprintfDouble5(buf,0.0/0.0));
}
Advertisement