Home > Net >  Keep prompting a user for a float if the input to scanf is invalid in C
Keep prompting a user for a float if the input to scanf is invalid in C

Time:05-21

I am trying to keep prompting a user for a float, until they enter a valid value.

Here is my code:

#include <stdio.h>
#include <stdlib.h>

int main() {

  float num;

  printf("Enter a float value: ");
  int success = scanf("%f", &num);

  while(!success) {
    fflush(stdin);
    printf("Enter a float value: ");
    success = scanf("%f", &num);
  }

  return 0;
}

I thought it was an issue with the stdin buffer so I tried flushing that. The weird thing is, fflush(stdin) works on macOS but not on linux.

On mac it will keep prompting a user for a float, but on linux it will get stuck in a loop, forever printing "Enter a float value: ". I have also tried fseek(stdin,0,SEEK_END); instead of fflush(stdin) and I get the same outcome.

My question is, how do I keep prompting a user for a valid float until they enter it?

CodePudding user response:

Your issue here, as you've spotted, is that scanf doesn't discard invalid input it doesn't consume. Therefore, the solution is to simply process it yourself.

#include <stdio.h>

void discard_line (void) {
  int c;
  do
    c = getchar();
  while (c != EOF && c != '\n');
}

int main (void) {
  float num;
  int rv;
  do {
    fputs("Enter a float value: ", stdout);
    rv = scanf("%f", &num);
    // discard the rest of the line, even if the input is valid
    // you might want to handle valid inputs differently
    discard_line();
  } while (!rv);
  // do whatever you want with the number
  return 0;
}

That discard_line function I wrote will simply consume all input until a newline (or EOF) is found. (Note that your code doesn't handle EOF at all, so you'll want to do that eventually. Consider adding a check at the beginning of the loop in main.) That way, your invalid input will be gone from the stdin buffer and scanf will read the next line.

CodePudding user response:

If you are reading any input with scanf() and you want to require the user to enter a valid float (and within any set value range), you want to loop continually until the user provide valid input. But, understand that canceling input with a manual EOF is also a valid user-action, so you should check for and handle that case is a graceful manner.

Another convenience is to provide a way the user can provide a character string as a prompt to be displayed, or none if NULL is passed instead of a character string.

A fairly straight forward approach would be something like:

/* read float value from stdin, optionally displaying 'prompt'.
 * on success the value at the address of 'n' is updated with the
 * float input, stdin is emptied and 1 is returned. Otherwise,
 * if the user cancels input with a manual EOF (ctrl   d, or on
 * windows ctrl   z) the value at 'n' is 0 and 0 is returned.
 */
int read_float_input (float *n, const char *prompt)
{
    *n = 0.;            /* (optional) set value at n to zero */

    for (;;) {          /* loop continually until good input or EOF */
        int rtn;        /* variable to hold scanf return */
        float tmp;      /* temporary float value to fill */

        if (prompt != NULL) /* display prompt if provided */
            fputs (prompt, stdout);

        rtn = scanf ("%f", &tmp);   /* attempt read, save return */

        /* validate scanf return */
        if (rtn == EOF) {   /* handle user canceled input */
            fputs ("(user canceled input).\n", stderr);
            return 0;
        }
        if (rtn == 1) {     /* good input, set *n = tmp, clean, return */
            *n = tmp;
            break;
        }
        /* handle matching-failure (input not an int) */
        fputs ("error: invalid float input.\n", stderr);
        empty_stdin();      /* remove chars causing failure */
    }

    empty_stdin();    /* tidy up to ready stdin for next input */
    return 1;
}

If you wanted to specify a range of values, you could add a min and max parameter and then adjust your check for good input, e.g.

        /* good input with range-check, set *n = tmp, clean, return */
        if (rtn == 1) {
          if (tmp < min || max < tmp) { /* handle out-of-range */
            fputs ("error: value out-of-range.\n", stderr);
            continue;
          }
          *n = tmp;
          break;
        }

You also need a way to empty any invalid characters from stdin (or your file stream) in the event of a matching failure, like if the user slips typing 456.89 and types r56.89. In that case, a matching failure would occur when 'r' was encountered, character extraction from the stream would cease leaving "r56.89" in your input buffer unread. To extract the offending character, you just read and discard characters until a '\n' or EOF is encountered. You can do that with something like the following (or just read using fgets() and perform the conversion using sscanf()), e.g.

/* if a matching-failure occurs while using scanf(), character
 * extraction from stdin ceases with the first offending chararacter
 * leaving the offending character(s) unread (likely leading to
 * an infinite loop if calling scanf() in a loop). To empty stdin
 * you must repeatedly read characters until '\n' or EOF is encountered.
 */
static void empty_stdin (void)
{
    int c = getchar();              /* read char from stdin */

    while (c != '\n' && c != EOF)   /* repeat until '\n' or EOF */
        c = getchar();
}

A short example putting the pieces together could be:

#include <stdio.h>

/* if a matching-failure occurs while using scanf(), character
 * extraction from stdin ceases with the first offending chararacter
 * leaving the offending character(s) unread (likely leading to
 * an infinite loop if calling scanf() in a loop). To empty stdin
 * you must repeatedly read characters until '\n' or EOF is encountered.
 */
static void empty_stdin (void)
{
    int c = getchar();              /* read char from stdin */

    while (c != '\n' && c != EOF)   /* repeat until '\n' or EOF */
        c = getchar();
}

/* read float value from stdin, optionally displaying 'prompt'.
 * on success the value at the address of 'n' is updated with the
 * float input, stdin is emptied and 1 is returned. Otherwise,
 * if the user cancels input with a manual EOF (ctrl   d, or on
 * windows ctrl   z) the value at 'n' is 0 and 0 is returned.
 */
int read_float_input (float *n, const char *prompt)
{
    *n = 0.;            /* (optional) set value at n to zero */

    for (;;) {          /* loop continually until good input or EOF */
        int rtn;        /* variable to hold scanf return */
        float tmp;      /* temporary float value to fill */

        if (prompt != NULL) /* display prompt if provided */
            fputs (prompt, stdout);

        rtn = scanf ("%f", &tmp);   /* attempt read, save return */

        /* validate scanf return */
        if (rtn == EOF) {   /* handle user canceled input */
            fputs ("(user canceled input).\n", stderr);
            return 0;
        }
        if (rtn == 1) {     /* good input, set *n = tmp, clean, return */
            *n = tmp;
            break;
        }
        /* handle matching-failure (input not an int) */
        fputs ("error: invalid float input.\n", stderr);
        empty_stdin();      /* remove chars causing failure */
    }

    empty_stdin();    /* tidy up to ready stdin for next input */
    return 1;
}

int main (void) {
  
  float f;
  
  if (!read_float_input (&f, "\nenter float: ")) {
    return 1;
  }
  
  printf ("\nvalid float value: %f\n", f);
}

Example Use/Output

With matching failures handled prior to good input:

$ ./bin/valid_float

enter float: float
error: invalid float input.

enter float: r56.89
error: invalid float input.

enter float: 456.89

valid float value: 456.890015

Or with user canceling by generating a manual EOF by pressing Ctrl d (or Ctrl z on windows) with an error code of 1 returned to the shell:

$ ./bin/valid_float

enter float: (user canceled input).
$ echo $?
1

  • Related