Loop Until Integer Input Is in Required Range Fails to Work with Non-Digit Character Inputs

Loop until integer input is in required range fails to work with non-digit character inputs

The problem is that:

cin >> input;

Will cause the bad bit to be set when you try and read a non numeric value. After that happens any attempt to use the operator>> is silently ignored.

So the way to correct for this is to test if the stream is in a good state and if not then reset the state flags and try and read again. But note that the bad input (that caused the problem) is still on the input so you need to make sure you throw it away as well.

if (cin >> input)
{
// It worked (input is now in a good state)
}
else
{
// input is in a bad state.
// So first clear the state.
cin.clear();

// Now you must get rid of the bad input.
// Personally I would just ignore the rest of the line
cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

// now that you have reset the stream you can go back and try and read again.
}

To prevent it getting stuck (which is caused by the bad bit being set) read into a string then use a string stream to parse user input. I also prefer this method (for user interactive input) as it allows for easier combination of different styles of reading (ie combining operator>> and std::getline() as you can use these on the stringstream).

#include <iostream>
#include <sstream>
#include <string>
// using namespace std;
// Try to stop using this.
// For anything other than a toy program it becomes a problem.

int main(int argc, char *argv[])
{
int input;

std::string line;
while(std::getline(std::cin, line)) // read a line at a time for parsing.
{
std::stringstream linestream(line);
if (!(linestream >> input))
{
// input was not a number
// Error message and try again
continue;
}
if ((input < 1) || (input > 3))
{
// Error out of range
// Message and try again
continue;
}
char errorTest;
if (linestream >> errorTest)
{
// There was extra stuff on the same line.
// ie sobody typed 2x<enter>
// Error Message;
continue;
}

// it worked perfectly.
// The value is now in input.
// So break out of the loop.
break;
}
}

Asking the user for input until they give a valid response

The simplest way to accomplish this is to put the input method in a while loop. Use continue when you get bad input, and break out of the loop when you're satisfied.

When Your Input Might Raise an Exception

Use try and except to detect when the user enters data that can't be parsed.

while True:
try:
# Note: Python 2.x users should use raw_input, the equivalent of 3.x's input
age = int(input("Please enter your age: "))
except ValueError:
print("Sorry, I didn't understand that.")
#better try again... Return to the start of the loop
continue
else:
#age was successfully parsed!
#we're ready to exit the loop.
break
if age >= 18:
print("You are able to vote in the United States!")
else:
print("You are not able to vote in the United States.")

Implementing Your Own Validation Rules

If you want to reject values that Python can successfully parse, you can add your own validation logic.

while True:
data = input("Please enter a loud message (must be all caps): ")
if not data.isupper():
print("Sorry, your response was not loud enough.")
continue
else:
#we're happy with the value given.
#we're ready to exit the loop.
break

while True:
data = input("Pick an answer from A to D:")
if data.lower() not in ('a', 'b', 'c', 'd'):
print("Not an appropriate choice.")
else:
break

Combining Exception Handling and Custom Validation

Both of the above techniques can be combined into one loop.

while True:
try:
age = int(input("Please enter your age: "))
except ValueError:
print("Sorry, I didn't understand that.")
continue

if age < 0:
print("Sorry, your response must not be negative.")
continue
else:
#age was successfully parsed, and we're happy with its value.
#we're ready to exit the loop.
break
if age >= 18:
print("You are able to vote in the United States!")
else:
print("You are not able to vote in the United States.")

Encapsulating it All in a Function

If you need to ask your user for a lot of different values, it might be useful to put this code in a function, so you don't have to retype it every time.

def get_non_negative_int(prompt):
while True:
try:
value = int(input(prompt))
except ValueError:
print("Sorry, I didn't understand that.")
continue

if value < 0:
print("Sorry, your response must not be negative.")
continue
else:
break
return value

age = get_non_negative_int("Please enter your age: ")
kids = get_non_negative_int("Please enter the number of children you have: ")
salary = get_non_negative_int("Please enter your yearly earnings, in dollars: ")

Putting It All Together

You can extend this idea to make a very generic input function:

def sanitised_input(prompt, type_=None, min_=None, max_=None, range_=None):
if min_ is not None and max_ is not None and max_ < min_:
raise ValueError("min_ must be less than or equal to max_.")
while True:
ui = input(prompt)
if type_ is not None:
try:
ui = type_(ui)
except ValueError:
print("Input type must be {0}.".format(type_.__name__))
continue
if max_ is not None and ui > max_:
print("Input must be less than or equal to {0}.".format(max_))
elif min_ is not None and ui < min_:
print("Input must be greater than or equal to {0}.".format(min_))
elif range_ is not None and ui not in range_:
if isinstance(range_, range):
template = "Input must be between {0.start} and {0.stop}."
print(template.format(range_))
else:
template = "Input must be {0}."
if len(range_) == 1:
print(template.format(*range_))
else:
expected = " or ".join((
", ".join(str(x) for x in range_[:-1]),
str(range_[-1])
))
print(template.format(expected))
else:
return ui

With usage such as:

age = sanitised_input("Enter your age: ", int, 1, 101)
answer = sanitised_input("Enter your answer: ", str.lower, range_=('a', 'b', 'c', 'd'))

Common Pitfalls, and Why you Should Avoid Them

The Redundant Use of Redundant input Statements

This method works but is generally considered poor style:

data = input("Please enter a loud message (must be all caps): ")
while not data.isupper():
print("Sorry, your response was not loud enough.")
data = input("Please enter a loud message (must be all caps): ")

It might look attractive initially because it's shorter than the while True method, but it violates the Don't Repeat Yourself principle of software development. This increases the likelihood of bugs in your system. What if you want to backport to 2.7 by changing input to raw_input, but accidentally change only the first input above? It's a SyntaxError just waiting to happen.

Recursion Will Blow Your Stack

If you've just learned about recursion, you might be tempted to use it in get_non_negative_int so you can dispose of the while loop.

def get_non_negative_int(prompt):
try:
value = int(input(prompt))
except ValueError:
print("Sorry, I didn't understand that.")
return get_non_negative_int(prompt)

if value < 0:
print("Sorry, your response must not be negative.")
return get_non_negative_int(prompt)
else:
return value

This appears to work fine most of the time, but if the user enters invalid data enough times, the script will terminate with a RuntimeError: maximum recursion depth exceeded. You may think "no fool would make 1000 mistakes in a row", but you're underestimating the ingenuity of fools!

Integer input validation - Rejecting non integer input and requesting another in C++?

Read a line of input as a std::string using std::getline().

Examine the string and check if it contains any characters that are not digits.

If the string only contains digits, use a std::istringstream to read an integer from the string. Otherwise report a failure, or take whatever other recovery action is needed (e.g. discard the whole string and return to read another one).

How to write the program that prints out all positive integers between 1 and given number , alternating between the two ends of the range?

This should work:

num = int(input("Please type in a number:"))
number_list = [i+1 for i in range(num)]

while number_list:
print(number_list.pop(0))
number_list.reverse()

C infinite loop when char input instead of int

Any time you are taking user-input, you must account for each character that remains in the input buffer (stdin here). This is especially true when taking input with scanf (or family) due to the way scanf handles input or matching failures. When either occurs, no further characters are read, and any offending characters are left in the input buffer unread -- just waiting to bite you again on your next attempted read (generally resulting in an infinite loop if you are taking input within a loop)

Further, you must understand how each conversion specier handles leading whitespace and whether leading whitespace is consumed by the specifier (e.g. numeric conversion specifiers and "%s") and which do not (all others, particularly "%c" and "%[...]").

This is one of the primary reasons a line-oriented function such as fgets (with an adequately sized buffer) or POSIX getline are recommended for taking user input. Both read and include the trailing '\n' in the buffer filled. This consumes the line completely, eliminating any uncertainty which additional characters remain in the input buffer unread. You can then pass the buffer to sscanf for parsing. This allows independent validation of both (1) the read ("did I get input?") and (2) the parse of information from the line ("did it contain the information I need?").

scanf can be used, if used correctly. This means you are responsible for checking the return of scanf every time. You must handle three conditions

  1. (return == EOF) the user canceled input by generating a manual EOF by pressing Ctrl+d (or on windows Ctrl+z, but see CTRL+Z does not generate EOF in Windows 10 (early versions));
  2. (return < expected No. of conversions) a matching or input failure occurred. For a matching failure you must account for every character that will be left in your input buffer. (scan forward in the input buffer reading and discarding characters until a '\n' or EOF is found); and finally
  3. (return == expected No. of conversions) indicating a successful read -- it is then up to you to check whether the input meets any additional criteria (e.g. positive integer, positive floating-point, within a needed range, etc..).

Utilizing that with your code, you could take input in the following way -- looping continually until valid input is received or the user cancels by generating a manual EOF, e.g.

#include <stdio.h>

void empty_stdin (void) /* simple helper-function to empty stdin */
{
int c = getchar();

while (c != '\n' && c != EOF)
c = getchar();
}

int main(void)
{
int input = 0,
rtn = 0; /* variable to save scanf return */
// domainEntry *myDomains = buildDomainDB();

for (;;) { /* loop continually until valid input or EOF */
printf ("\nSelect top level domain:\n"
" 1-EDU\n"
" 2-COM\n"
" 3-ORG\n"
" 4-GOV\n"
" 5-MIL\n"
" 6-CN\n"
" 7-COM.CN\n"
" 8.CAN\n\n"
"choice: ");
rtn = scanf (" %d", &input); /* save return */

if (rtn == EOF) { /* user generates manual EOF */
fputs ("(user canceled input.)\n", stderr);
return 1;
}
else if (rtn == 0) { /* matching failure */
fputs (" error: invalid integer input.\n", stderr);
empty_stdin();
}
else if (input < 1 || 8 < input) { /* validate range */
fputs (" error: integer out of range [1-8]\n", stderr);
empty_stdin();
}
else { /* good input */
empty_stdin();
break;
}
}

printf ("\nvalid input: %d\n", input);
}

(note the use of the helper-function empty_stdin() -- and that even after a successful read, you should still empty_stdin() to insure the input buffer is empty and prepared for the next user input -- whatever that may be)

Example Use/Output

$ ./bin/getintmenu

Select top level domain:
1-EDU
2-COM
3-ORG
4-GOV
5-MIL
6-CN
7-COM.CN
8.CAN

choice: edu
error: invalid integer input.

Select top level domain:
1-EDU
2-COM
3-ORG
4-GOV
5-MIL
6-CN
7-COM.CN
8.CAN

choice: 9
error: integer out of range [1-8]

Select top level domain:
1-EDU
2-COM
3-ORG
4-GOV
5-MIL
6-CN
7-COM.CN
8.CAN

choice: 4

valid input: 4

If you do your job, you can successfully use scanf as needed.

Or, you can make your life much easier and use fgets and then test whether the first character in the buffer (by simply dereferencing the pointer) is a valid menu selection, e.g.

#include <stdio.h>

#define MAXC 1024 /* read buffer max characters */

int main (void) {

int input = 0;
char buf[MAXC];
// domainEntry *myDomains = buildDomainDB();

for (;;) { /* loop continually until valid input or EOF */
fputs ("\nSelect top level domain:\n"
" 1-EDU\n"
" 2-COM\n"
" 3-ORG\n"
" 4-GOV\n"
" 5-MIL\n"
" 6-CN\n"
" 7-COM.CN\n"
" 8.CAN\n\n"
"choice: ", stdout);
if (!fgets (buf, MAXC, stdin)) {
fputs ("(user canceled input.)\n", stderr);
return 1;
}

if (*buf < '1' || '8' < *buf) { /* check 1st char, validate range */
fputs (" error: invalid input\n", stderr);
continue;
}

input = *buf - '0'; /* convert char to integer */
break;
}

printf ("\nvalid input: %d\n", input);
}

Of course if a key gets stuck and the user enters more than 1023 characters -- characters will remain in the input buffer. But a simple test of whether the last character is '\n' and if not whether MAXC - 1 characters were read will let you know whether that is the case. Your choice, but fgets provides a much easier implementation. Just remember -- don't skimp on buffer size. I'd rather have a buffer 10,000 bytes too long that 1-byte too short....

C programming - Loop until user inputs number scanf

scanf returns the count of items that it has successfully read according to your format. You can set up a loop that exits only when scanf("%d", &number2); returns 1. The trick, however, is to ignore invalid data when scanf returns zero, so the code would look like this:

while (scanf("%d",&number2) != 1) {
// Tell the user that the entry was invalid
printf("You did not enter a valid number\n");
// Asterisk * tells scanf to read and ignore the value
scanf("%*s");
}

Since you read a number more than once in your code, consider making a function to hide this loop, and call this function twice in your main to avoid duplication.



Related Topics



Leave a reply



Submit