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