Reputation: 75906
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.
Upvotes: 4
Views: 2221
Reputation: 133
I created a small library Double2String to address this specific issue:
https://github.com/coconut2015/double2string
Upvotes: 0
Reputation: 14205
If you really want base-10 floating-point output, it's probably easiest to write a JNI wrapper for C's printf
here. The Java folks decided they needed to do printf
themselves. Apart from what you've already noticed about %g
, they decided to change the rounding behaviour and truncate output in a curious way. To wit:
System.out.printf("%.5g\n", 1.03125);
System.out.printf("%.5g\n", 1.09375);
1.0313
1.0938
gcc
correctly rounds to even:
printf("%.5g\n", 1.03125);
printf("%.5g\n", 1.09375);
1.0312
1.0938
Notice that 1.03125 and 1.09375 are exactly representable as doubles since 1/32 = 0.3125.
Java's printf %g format wrongly truncates its output:
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
Here's the right answer:
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
is normal but not exactly representable. Java pretends not to notice:
System.out.printf("%.20g\n%.20a\n", 1.0e-200, 1.0e-200);
1.0000000000000000000e-200
0x1.87e92154ef7ac0000000p-665
Here's the right answer:
printf("%.20g\n%.20a\n", 1.0e-200, 1.0e-200);
9.999999999999999821e-201
0x1.87e92154ef7ac0000000p-665
So you've either got to live with bizarro rounding behaviour in your printf or you piggyback off gcc
and glibc
's work. I can't recommend trying to print out floating-point numbers by yourself. Or you can just use %a
, which AFAIK works perfectly fine in Java.
Upvotes: 7
Reputation: 75906
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("%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));
}
Upvotes: 5
Reputation: 51039
This code
#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);
}
gave me
1234
123.46
0.00012346
1.2346e-13
and this code
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));
}
}
gave me
1234,0
123,46
0,00012346
1,2346e-13
comma is because of my regional settings. So there is only one difference.
Upvotes: 1