If you want the result as a string, you should probably print to a string with extra precision, then chop that off yourself. (See @chux's answer for details on how much extra precision you need for IEEE 64-bit double
to avoid rounding up from a string of 9s, since you want truncation but all the usual to-string functions round to nearest.)
If you want a double
result, then are you sure you really want this? Rounding / truncating early in the middle of a calculation usually just worsens the accuracy of the final result. Of course, there are uses in real algorithms for floor/ceil, trunc, and nearbyint, and this is just a scaled version of trunc.
If you just want a double
, you can get fairly good results without ever going to a string. Use ndigits
and floor(log10(fabs(x)))
to work out a scale factor, then truncate the scaled value to an integer, then scale back.
Tested and working (with and without -ffast-math
). See the asm on the Godbolt compiler explorer. This might run reasonably efficiently, especially with -ffast-math -msse4.1
(so floor and trunc can inline to roundsd
).
If you care about speed, look into replacing pow()
with something that takes advantage of the fact that the exponent is a small integer. I'm not sure how fast library pow() implementations are in that case. GNU C __builtin_powi(x, n)
trades accuracy for speed, for integer exponents, doing a multiplication tree, which is less accurate than what pow()
does.
#include <float.h>
#include <math.h>
#include <stdio.h>
double truncate_n_digits(double x, int digits)
{
if (x==0 || !isfinite(x))
return x; // good idea stolen from Chux's answer :)
double l10 = log10(fabs(x));
double scale = pow(10., floor(l10) + (1 - digits)); // floor rounds towards -Inf
double scaled = x / scale;
double scaletrunc = trunc(scaled); // trunc rounds towards zero
double truncated = scaletrunc * scale;
#if 1 // debugging code
printf("%2d %24.14g =>\t%24.14g\t scale=%g, scaled=%.30g\n", digits, x, truncated, scale, scaled);
// print with more accuracy to reveal the real behaviour
printf(" %24.20g =>\t%24.20g\n", x, truncated);
#endif
return truncated;
}
test cases:
int main() {
truncate_n_digits(0.014568, 1);
truncate_n_digits(0.246456, 1);
truncate_n_digits(0.014568, 2);
truncate_n_digits(-0.246456, 2);
truncate_n_digits(1234567, 2);
truncate_n_digits(99999999999, 6);
truncate_n_digits(-99999999999, 6);
truncate_n_digits(99999, 10);
truncate_n_digits(-0.0000000001234567, 3);
truncate_n_digits(1000, 6);
truncate_n_digits(0.001, 6);
truncate_n_digits(1e-312, 2); // denormal, and not exactly representable: 9.999...e-313
truncate_n_digits(nextafter(1e-312, INFINITY), 2); // denormal, just above 1.00000e-312
return 0;
}
each result shown twice: first with only %.14g
so rounding gives the string we want, then again with %.20g
to show enough places to reveal the realities of floating point math. Most numbers are not exactly-representable, so even with perfect rounding it's impossible to return a double
exactly represents the truncated decimal string. (Integers up to about the size of the mantissa are exactly representable, and so are fractions where the denominator is a power of 2.)
1 0.014568 => 0.01 scale=0.01, scaled=1.45679999999999987281285029894
0.014567999999999999353 => 0.010000000000000000208
1 0.246456 => 0.2 scale=0.1, scaled=2.46456000000000008398615136684
0.2464560000000000084 => 0.2000000000000000111
2 0.014568 => 0.014 scale=0.001, scaled=14.5679999999999996163069226895
0.014567999999999999353 => 0.014000000000000000291
2 -0.246456 => -0.24 scale=0.01, scaled=-24.6456000000000017280399333686
-0.2464560000000000084 => -0.23999999999999999112
3 1234.56789 => 1230 scale=10, scaled=123.456789000000000555701262783
1234.567890000000034 => 1230
6 1234.56789 => 1234.56 scale=0.01, scaled=123456.789000000004307366907597
1234.567890000000034 => 1234.5599999999999454
6 99999999999 => 99999900000 scale=100000, scaled=999999.999990000040270388126373
99999999999 => 99999900000
6 -99999999999 => -99999900000 scale=100000, scaled=-999999.999990000040270388126373
-99999999999 => -99999900000
10 99999 => 99999 scale=1e-05, scaled=9999900000
99999 => 99999.000000000014552
3 -1.234567e-10 => -1.23e-10 scale=1e-12, scaled=-123.456699999999983674570103176
-1.234566999999999879e-10 => -1.2299999999999998884e-10
6 1000 => 1000 scale=0.01, scaled=100000
1000 => 1000
6 0.001 => 0.001 scale=1e-08, scaled=100000
0.0010000000000000000208 => 0.0010000000000000000208
2 9.9999999999847e-313 => 9.9999999996388e-313 scale=1e-314, scaled=100.000000003458453079474566039
9.9999999999846534143e-313 => 9.9999999996388074622e-313
2 1.0000000000034e-312 => 9.0000000001196e-313 scale=1e-313, scaled=9.9999999999011865980946822674
1.0000000000034059979e-312 => 9.0000000001195857973e-31
Since the result you want will often not be exactly representable, (and because of other rounding errors) the resulting double
will sometimes be below the result you want, so printing it with full precision might give 1.19999999 instead of 1.20000011. You might want to use nextafter(result, copysign(INFINITY, original))
to get a result that's more likely to have a higher magnitude than what you want.
Of course, that could just make things worse in some cases. But since we truncate towards zero, most often we get a result that's just below (in magnitude) the unrepresentable exact value.