Set Precision and Clip Trailing Zeros but Never Print Exponent
Asked Answered
S

4

3

I need to:

  1. Set precision so that floats are rounded to the hundredths place ( 0.111 prints as 0.11 )
  2. Clip trailing zeros ( 1.0 prints as 1 )
  3. Never print an exponent ( 1000.1 prints as 1000.1 )

printf( "%.2f\n", input ); // handles 1 and 3 but not 2
printf( "%.2g\n", input ); // handles 1 and 2 but not 3
cout << setprecision( 2 ) << input << endl; // handles 1 and 2 but not 3

Is there a printf or cout option that will let me handle all of these?

Selmore answered 20/8, 2014 at 11:38 Comment(4)
I don't understand how printf( "%.2f\n", fails at point 3. It prints no exponent when I use it (Clang on Mac OS X) to print either 1E267 or 1E-267.Lubeck
@PascalCuoq thanks I had changed the order, and forgot to change the attached code. I've fixed it now.Selmore
@Jongware I almost did the same thing you did, then I considered that a pure C solution was perhaps acceptable to the OP, even if the question does contain some C++. Now I have written a solution in pure C and your edit makes my solution look off-topic.Lubeck
@Pascal: ... okay, agreed I was too hastely. The OP could have been just a bit more clear on this. (I have seen posters complain when asking for C++ and handed a pure C solution, which IMO indeed should be at least as good.)Deferral
S
0

Sadly there is no way to force streams to use printf's %f behavior. So the only way to handle this is trimming decimal places manually as necessary. I've added a C++ code sample that handles this:

string fullfloat(static_cast<size_t>(log10(numeric_limits<declval(input)>::max())) + 5U, '\0'); // Adding 1 for the 10s place, 1 for the '.' 2 for the decimal places, and 1 for the null
const size_t length = size(fullfloat) - sprintf(data(fullfloat), "%.2f", input );

*(--mismatch(rbegin(fullfloat) + length, next(rbegin(fullfloat), 3U + length), "00.").first) = '\0';

fullfloat will now contain the correct string, but because it's size will extend past the applied '\0' character the only way to get it to print property would be to use data():

cout << data(fullfloat);
Selmore answered 21/8, 2014 at 14:58 Comment(0)
L
4

The C11 standard says of %f and %F (7.21.6.1:8):

A double argument representing a floating-point number is converted to decimal notation in the style [−]ddd.ddd, where the number of digits after the decimal-point character is equal to the precision specification. If the precision is missing, it is taken as 6; if the precision is zero and the # flag is not specified, no decimal-point character appears. If a decimal-point character appears, at least one digit appears before it. The value is rounded to the appropriate number of digits.

Here is a C snippet that produces what you want in a malloced bloc t, that you need to free up afterwards. If you are writing in C99, a variable-length array can also be considered.

The code below does not introduce any additional approximation to the conversion (if your printf prints the correctly rounded conversion to decimal, so will the code below), and works for all floating-point values.

#include <stdio.h>
#include <stdlib.h>
…
int len = snprintf(0, 0, "%.2f", input);
if (len < 0) fail();
char *t = malloc((size_t)len + 1);
if (!t) fail();
if (sprintf(t, "%.2f", input) < 0) fail();
len--;
if (t[len] == '0') {
  len--;
  if (t[len] == '0') {
    len--;
    if (t[len] == '.') len--;
  }
  t[len + 1] = '\0';
}
Lubeck answered 20/8, 2014 at 11:59 Comment(8)
So the only reason for my use of sprintf is my need for printing a float without exponent. I'm working up some C++ now to accomplish what you're doing with C above. If you feel like doing this in C++ style I'll accept your answer, if not I can post my own answer.Selmore
@JonathanMee Sorry, I cannot provide a C++ answer, but if you needed one you really had no reason to tag your question as “C”. In fact, followers of both these tags (me and Jongware, at least) would be thankful if you avoided tagging your future questions with both tags unless they are really both relevant to your question.Lubeck
It seems that any solution I use for this problem must use a printf variant. I wan't event aware of snprintf. I had hoped that there would be some other old school stdio.h magic that would solve this problem without me having to write code to trim the trailing 0s. It doesn't look like that's the case, but that was my rationale for using the C tag.Selmore
@JonathanMee: did you see the pretty awesome answers (at least some of them) to How to implement "char *ftoa(float num" without sprintf() library function? You can probably adjust one of them to your taste.Deferral
@Deferral An elegant, minimalistic, correctly rounded solution (unlike all the dubious answers from the question you link to) can be found in the musl library: git.musl-libc.org/cgit/musl/tree/src/internal/floatscan.cLubeck
@Pascal: that's even better! I intended to find a library implementation but .. well .. the first hit was that SO answer .. (Are the answers really that bad? (Some are, I can see that.))Deferral
@Deferral The conversion of binary floating-point to and from decimal has, unsurprisingly, been studied for years (decades now). The standard references are a bit dated in the vocabulary they use and focus on optimizations that remove in simplicity what they add in speed for common cases. Musl is a good source for understandable algorithms, because the main contributor forced himself to re-invent everything by himself before looking at the literature, and another good source is the blog at exploringbinary.comLubeck
@Deferral What I really want is the opposite of #2303469, I'm trying to find an option in functionality that exists within C++ which will solve my problem. I don't want to reinvent the wheel. So to me Pascal Quoq's solution is much preferable to any of those solutions. Pascal Cuoq's solution could be improved upon though by using <float.h>'s FLT_MAX and the log trick I do in my answer. That would remove the need for snprintf and malloc.Selmore
S
1

I am not aware of any format specifier that will do what you are looking for.

Pre-digesting the values before passing them to separate format specifiers might work. For example:

  • Multiply the original floating point number by 100 and round to the nearest integer
  • Assign to nScaled (int).
  • Assign mod(nScaled,100) to another integer, nFractional.
  • Assign nScaled/100 to another integer, nWhole.

if( nFractional > 0 )
  printf("%d.%d", nWhole, nFractional );
else
  printf("%d", nWhole );

You probably already know this.

Springhead answered 20/8, 2014 at 11:54 Comment(2)
The issues with this are that the multiplication by 100 can be inexact, and that the result does not have to fit an int or any available integer type. You might as well erase trailing zeroes and dots from a string to which you have printed the floating-point number with %f, at least it will work for all numbers.Lubeck
Also it produces the wrong result when 1 ≤ nFractional ≤ 9.Lubeck
H
0

I thought another option is to use asprintf() function: it dynamically allocate a string of proper length by itself. Once the string is stored trailing zeros/dot could be cut off:

...
float num;
...

char *digits;
int i=asprintf(&digits, "%.2f", num)-1;

for(; digits[i] !='.'; i--)
  if (digits[i] == '0') digits[i] = NULL; else break;

if (digits[i] == '.') digits[i] = NULL;

printf("%s\n", digits);
free(digits);
...
Hell answered 20/8, 2014 at 16:22 Comment(1)
This could be workable on a Linux platform. But this is really inferior to @PascalCuoq or my solution because of it's lack of portability.Selmore
S
0

Sadly there is no way to force streams to use printf's %f behavior. So the only way to handle this is trimming decimal places manually as necessary. I've added a C++ code sample that handles this:

string fullfloat(static_cast<size_t>(log10(numeric_limits<declval(input)>::max())) + 5U, '\0'); // Adding 1 for the 10s place, 1 for the '.' 2 for the decimal places, and 1 for the null
const size_t length = size(fullfloat) - sprintf(data(fullfloat), "%.2f", input );

*(--mismatch(rbegin(fullfloat) + length, next(rbegin(fullfloat), 3U + length), "00.").first) = '\0';

fullfloat will now contain the correct string, but because it's size will extend past the applied '\0' character the only way to get it to print property would be to use data():

cout << data(fullfloat);
Selmore answered 21/8, 2014 at 14:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.