1

I am looking for a sprintf-like format string that would format float numbers with a variable number of decimals, but a fixed total number of characters (say, 3), in order to provide maximum information. For instance:

0      ->  `.00` or `0.0` or `  0`
0.124  ->  `.12`
0.357  ->  `.36`
1.788  ->  `1.8`
9.442  ->  `9.4`
10.25  ->  `10.` or ` 10`
75.86  ->  `76.` or ` 76`
99.44  ->  `99.` or ` 99`
100.0  ->  `100`

(yes, my numbers will all be 0-to-100 floats)

How to achieve this?
Is this kind of fixed-width formatting implemented in sprintf format string language?

iago-lito
  • 3,098
  • 3
  • 29
  • 54
  • .00 , 1.5 and 200 doesn't share something common.Do you want the output as a string? – Tsakiroglou Fotis Nov 14 '18 at 09:07
  • @TsakiroglouFotis They share that they all are 3-chars long. Yes, the output is a string :) Do I need to write the method myself? Is it out of sprintf capabilities? – iago-lito Nov 14 '18 at 09:08
  • 1
    Research `"%3g"` as that seems to get you close to your goal. – chux - Reinstate Monica Nov 14 '18 at 12:40
  • @chux As I've written, my numbers only range from 0 to 100, I agree that critical information loss is unavoidable when trying to squish `-999` in only 3 chars. – iago-lito Nov 14 '18 at 12:42
  • @chux "%3g" is interesting indeed, but not exactly what I need. Maybe the format string I need just doesn't exist? – iago-lito Nov 14 '18 at 12:43
  • Your test cases do not include tricky ones like 99.999, 9.9999, 0.9999. These "just under a power of 10" values tend to mess up candidate solutions. – chux - Reinstate Monica Nov 14 '18 at 13:08
  • @chux I expect all those to be rounded up to `100`, `10.` or ` 10`, and `1.0`. – iago-lito Nov 14 '18 at 13:12
  • @chux because `printf("%3g", 75.86);` yields `75.86`, not `76.` or ` 76`. (wops: I had a rounding error in the OP, sorry for that) – iago-lito Nov 14 '18 at 13:14
  • If that rounding is significant you'd need to establish the number of significant digits *before* you call `*printf( ..."%*f"...)`, and my answer would need a redesign. Note you can set rounding behavior with [`fesetround()`](https://en.cppreference.com/w/c/numeric/fenv/feround), but `*printf()` needs to know how many significant digits to round to. – DevSolar Nov 14 '18 at 13:29
  • [Writing IEEE 754-1985 double as ASCII on a limited 16 bytes string](https://stackoverflow.com/q/32631178/2410359) may help. – chux - Reinstate Monica Nov 14 '18 at 19:05

2 Answers2

3

Is this kind of fixed-width formatting implemented in sprintf format string language?

No.

How to achieve this?

To meet OP's goals, code can analyze the range of the float f to attempt to print with maximal information.

This begins with "%.*f" to control the number of digits following the ..

A common pitfall occurs with values just under a power of 10 that round up and cause another digit in the output.

Code could try to test against f < 0.995f rather than f < 1.0 for example, yet this leads into failed corner cases given the binary nature of floating point.

Better to test the range against an exact constant like 1.0, 10.0, ...

The below tries, at most twice, to sprintf(). The 2nd attempt occurs with those pesky values just under a power of 10.

#include <assert.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>

void iago_sprint1(char *dest, int len, float f) {
  if (f < 1.0) {
    char buf[len + 2];
    //printf("p1\n");
    snprintf(buf, sizeof buf, "%.*f", len - 1, f);
    if (buf[0] == '0') {
      strcpy(dest, buf + 1);
      return;
    }
  }
  float limit = 10.0;
  int prec = len - 2;

  while (prec >= -1) {
    if (f < limit) {
      char buf[len + 2];
      //printf("p2\n");
      int cnt = snprintf(buf, sizeof buf, "%.*f", prec < 0 ? 0: prec, f);
      if (cnt <= len) {
        strcpy(dest, buf);
        return;
      }
    }
    prec--;
    limit *= 10;
  }
  assert(0); // `f` was out of range
}

#define N 3
int main(void) {
  float f[] = {0, 0.124f, 0.357f, 1.788f, 9.442f, 10.25f, 75.86f, 99.44f,
      100.0f, 99.999f, 9.9999f, .99999f, .099999f, 1.04f, 1.06f};
  char line[N + 1];

  for (unsigned i = 0; i < sizeof f / sizeof *f; i++) {
    //snprintf(line, sizeof line, "%#*g", N, f[i]);
    //puts(line);

    iago_sprint1(line, N, f[i]);
    puts(line);
  }
}

Output

.00
.12
.36
1.8
9.4
10
76
99
100
100
10
1.0
.10
1.0
1.1

Pre-calculating precision needs pitfall

Should code attempt to pre-calculate the precision needed for only 1 sprintf() call, the calculation needs to deduce precision just as sprintf() does - in effect code is doing sprint()` work over again.

Consider len==3 and float x = 9.95f;. As a binary float does not represent that exactly, it instead has a value just above or below 9.951. If it is below, the string should be "9.9", if it is above, "10.". If code had double x = 9.95; (again not exactly representable) the output might differ. If code used a float, but FLT_EVAL_MODE > 1, the actual value passed might not be the expected float 9.95.

Precision-->  .1   .0 
9.94001...   "9.9"  "10."
9.94999...   "9.9"  "10." // Is this 9.95f
9.95001...   "10.0" "10." // or this?
9.95999...   "10.0" "10."

New and improved

I re-worked and simplified code - after acceptance.

The trick is to print with "%*.f" a calculated precision based on the power-of-10 of f to a buffer one wider than the goal - assuming no carry into another digit due to rounding.

The power-of-10 calculation can be done exactly with a small loop.

When the leading character is a 0, it is not needed as in "0.xx" --> ".xx".

Otherwise with no carry into another digit due to rounding, the string fits and we are done.

Otherwise a carry, then the last character is either a '.' or '0' after the decimal point and so not needed. This happens when f is just below a power of 10, but the printed version rounds up to that power of 10. And so only copy length-1 digits.

// `len` number of characters to print
// `len+1` is the size of `dest`
void iago_sprint3(char *dest, int len, float f) {
  assert(len >= 1);
  int prec = len - 1;
  float power10 = 1.0;
  while (f >= power10 && prec > 0) {
    power10 *= 10;
    prec--;
  }

  char buf[len + 2];
  int cnt = snprintf(buf, sizeof buf, "%.*f", prec, f);
  assert (cnt >= 0 && cnt <= len + 1);
  if (buf[0] == '0') {
      strcpy(dest, buf + 1);
      return;
    }
  strncpy(dest, buf, (unsigned) len);
  dest[len] = 0;
}

1 Typical float 9.95f is exactly

9.94999980926513671875    

Typical double 9.95 is exactly

9.949999999999999289457264239899814128875732421875
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • Great, cheers :) I suspected I'd have to write it myself, but didn't suspected it would be such a big work. Thank you again! I didn't get your line about monotony though, this rounding function sure is monotonic, right? – iago-lito Nov 14 '18 at 22:27
  • Simpler with a lot of input work. I really appreciate your effort :) – iago-lito Nov 15 '18 at 10:45
0

You could maybe try:

void mysprintf(float a) {
    if (a < 10 && a >= 0) {
        printf("%2f", a);
    } else if (a >= 10 && a < 100) {
        printf("%1f", a);

    } else {
        printf("%d", (int)a);
    }
}
OznOg
  • 4,440
  • 2
  • 26
  • 35
Sari Masri
  • 206
  • 1
  • 10
  • Okay, so you mean that there is no sprintf format string to achieve what I need, and I need to format the string myself, right? :) – iago-lito Nov 14 '18 at 09:14
  • this is a custom sprintf to your situation :) – Sari Masri Nov 14 '18 at 09:17
  • I get it. Cheers :) Don't hesitate to flesh your answer a little bit. Also, since the question is about a sprintf formatting string and not a dedicated method, are you sure that there is no `fstring` such that `printf(fstring, a)` would always produce the desired output? – iago-lito Nov 14 '18 at 09:19
  • I think there is no fstring in c language but also we can build it, thank you for your suggestion – Sari Masri Nov 14 '18 at 09:27
  • 1
    If you really think so, then I think it is worth making it explicit in your answer :) – iago-lito Nov 14 '18 at 09:28
  • 3
    `printf("%d",a);` is undefined behavior, `printf("%d", (int)a);` would work. – KamilCuk Nov 14 '18 at 09:39
  • Sari Masri, `a = 1.0 printf("%2f", a);` --> `"1.000000"`. Perhaps you are thinking of `printf("%.2f", a);`. Yet that has troubles too. – chux - Reinstate Monica Nov 14 '18 at 18:45