It's good that you have a solution, but it does not do what you said you
wanted!
It does; however, he should be using a much higher precision so as not
to lose significant digits. I would propose, say:
"%." DBL_DIG "g"
This approach is exactly how this works:
$ txr -e '(format t "~s" 3.0)'
3.0
$ txr -e '(format t "~a" 3.0)'
3
In the case of "~s", the ".0" is actually *added* deliberately, so that there
is print/read consistency: the printed representation is a floating-point
number.
How this works is that there is a sprintf with a "%.*g" conversion, where
the argument to the * is a variable precision. Its default value is DBL_DIG.
But, of course %g will use either %f or %e as appropriate, and so we
can get results like:
$ txr -p '(format nil "~a" 0.000000000000000001)'
1e-18
You can see this in the vformat function in this source file:
http://www.kylheku.com/cgit/txr/tree/stream.c
Look for "case 'a'".
Here is the thing. Although an approximate description of %g is that it works
by using the formatting of either %f or %e, this is not exactly true: %f does
not have the %g behavior of removing trailing zeros in the fractional part,
including, possibly the decimal separator itself. %f also interprets the
precision parameter differently: precision to %f represents the number of
digits after the decimal point, whereas %g treats precision as the number of
significant figures.
If you must always have output in the form \d+([.]\d+)? where the (\d+)? part
is omitted if the digits are all zero, and has no trailing zeros, then
use sprintf to format the number into a buffer, and do some text processing
on it. You can integrate it into printf like this:
char scratch[64]; /* or whatever */
printf("%s\n", format_num(3.0, 5, scratch)); /* up to 5 digits past point */
format_num returns a pointer into the scratch buffer where it placed the
formatted number. You can further adjust that with some width and precision
on the %s.s. format_num can be written so that it (perhaps optionally)
obliterates the unwanted decimal parts with spaces:
Hypothetical run:
/* 1 tells format: "replace trailing stuff with spaces; do not chop". */
printf("%10s\n", format_num(3.14, 5, 1, scratch));
printf("%10s\n", format_num(3.0, 5, 1, scratch));
Output: right adjusted in a field of 10 by %s:
___3.14___ <- underscores represent spaces
___3______
The point is that the numbers are still nicely aligned.
Possible implementation of format_num:
#include <string.h>
#include <stdio.h>
/* scratch is assumed to be 64 bytes */
char *format_num(double val, int fracdig, int use_spaces, char *scratch)
{
int chars = snprintf(scratch, 64, "%.*f", fracdig, val);
char *dot = strchr(scratch, '.');
if (chars < 0 || /* old, nonconforming sprintf libraries */
chars >= 64)
return "#.#"; /* could not format number */
if (!dot)
return scratch;
fracdig = strlen(dot+1); /* these should be equal, but just in case */
if (strspn(dot+1, "0") == fracdig) {
/* decimal is all zeros */
if (use_spaces)
memset(dot, ' ', strlen(dot));
else
*dot = 0;
} else {
int nonzeros = strcspn(dot+1, "0");
if (nonzeros < fracdig) {
if (use_spaces)
memset(dot + 1 + nonzeros, ' ', fracdig - nonzeros);
else
*(dot + 1 + nonzeros) = 0;
}
}
return scratch;
}
int main(void)
{
char sch[64];
/* pitiful test suite */
printf("[%10s]\n", format_num(3.0, 5, 1, sch));
printf("[%10s]\n", format_num(3.14, 5, 1, sch));
printf("[%10s]\n", format_num(3.0, 5, 0, sch));
printf("[%10s]\n", format_num(3.14, 5, 0, sch));
printf("[%10s]\n", format_num(3.0, 0, 1, sch));
printf("[%10s]\n", format_num(3.14, 0, 1, sch));
printf("[%10s]\n", format_num(3.0, 0, 0, sch));
printf("[%10s]\n", format_num(3.14, 0, 0, sch));
printf("[%10s]\n", format_num(3.14e50, 0, 0, sch));
printf("[%10s]\n", format_num(3.14e120, 0, 0, sch));
return 0;
}
Output:
[ 3 ]
[ 3.14 ]
[ 3]
[ 3.14]
[ 3]
[ 3]
[ 3]
[ 3]
[314000000000000011495964840544938881872998818643968]
[ #.#]