6.3. Interactive while Loops

Next we consider a particular form of while loops, interactive ones, involving input from the user each time through. We consider them now for three reasons:

  • Interactive while loops have one special ‘gotcha’ worth illustrating.
  • We will illustrate some general techniques for understanding and developing while loops.
  • As a practical matter, we can greatly improve the utility input functions we have been using, and add some more.

We already have discussed the InputInt function. The user can choose any int. Sometimes we only want an integer in a certain range. Miles has examples that handle this by just silently changing a bad value to one at the end of the allowed range. Another approach is to not accept a bad value, and get the user to explicitly enter a value in the right range. In theory the user could make errors for some time, so a loop makes sense. For instance we might have a slow user, and there could be an exchange like the following when you want a number from 0 to 100. For illustration, user input is shown in boldface:

Enter a score: (0 to 100) 233
233 is out of range!
Enter a score: (0 to 100) 101
101 is out of range!
Enter a score: (0 to 100) -1
-1 is out of range!
Enter a score: (0 to 100) 100

and the value 100 would be accepted.

This is a well-defined idea. A function makes sense. Its heading includes a prompt and low and high limits of the allowed range:

   /** Continue to obtain a value from the user until it is in the
    * range [lowLim, highLim].  Then return the value in range.
    * Use the specified prompt, adding a reminder of the allowed range. */
   static int InputIntInRange(string prompt, int lowLim, int highLim)

There is no need to put the limits in the prompt, since the function can create that part from the limit parameters. For example to generate sequence above, the call would be:

InputIntInRange("Enter a score: ", 0, 100)

There is an issue with the common term “loop” in programming. In normal English, a loop has no beginning and no end, like a circle. C# loops have a sequence of statements with a definite beginning and end.

Consider the sequence above in pseudocode.

Input a number with prompt (233)
Print error message
Input a number with prompt (101)
Print error message
Input a number (-1)
Print error message
Input a number with prompt (100)
Return 100

We can break into a repeating pattern in two ways. The most obvious is the following, with three repetitions of a basic pattern, with the last two line not in the same pattern (so they would go after the loop). :

Input a number with prompt (233)
Print error message

Input a number with prompt (101)
Print error message

Input a number (-1)
Print error message

Input a number with prompt (100)
Return 100

Another choice, since you can split a loop at any point, would be the following, with the first and last lines not in the pattern repeating three times in the middle:

Input a number with prompt (233)

Print error message
Input a number with prompt (101)

Print error message
Input a number (-1)

Print error message
Input a number with prompt (100)

Return 100

When you consider while loops, there is a problem with the first version: Before the first pass through the loop and at the end of the block of code in the body of the loop, you must be able to run the test in the while heading. We will be testing the latest input from the user.

It is the second version that has us getting new input before the first loop and at the end of each loop!

Now we can think more of the basic process to turn this into a C# solution: What variables do we need? We will call the user’s response number.

What is the test in the while loop heading? The easiest thing to think of is that we are done when we get something correct. That, however, is a termination condition. We need to reverse it to get the continuation condition, that the answer is out of range. There are two ways to be out of range:

number < lowLim
number > highLim

How do we combine them? Either one rules out a correct answer, so number is out of range if too high OR too low. Remember the C# symbolism for “or”: ||:

while (number < lowLim || number > highLim) {

Following the sequence in the concrete example we had above, we can see how to put things together. We need to get input from the user before first beginning the while loop, so we immediately have something to test in the while heading’s condition.

Do not reinvent the wheel! We can use our earlier general IntInput function. It needs a prompt. As a first version, we can use the parameter prompt:

int number = InputInt(prompt);

That is the initialization step before the loop.

If we get into the body of the loop, it means there is an error, and the concrete example indicates we print a warning message. The concrete example also shows another step in the loop, asking the user for input. It is easy to think “I already have the code included to read a value from the user, so there is nothing really to do.” WRONG! The initialization code with the input from the user is before the loop. C# execution approaches the test in the while headings from two places: the initialization and coming back from the bottom of the loop. To get a new value to test, we must repeat getting input from the user at the bottom of the loop body.

You might decide to be quick and just copy the initialization line into the bottom of the loop (and indent it):

int number = InputInt(prompt);

Luckily you will get a compiler error in that situation, avoiding more major troubleshooting: The complete copy of the line copies the declaration part as well as the assignment part, and mono sees the declaration of number already there from the scope outside the while block, and complains.

Hence copy the line, minus the `` int`` declaration:

number = InputInt(prompt);

When the loop condition becomes false, and you get past the loop, you have a correct value in number. You have done all the hard work. Do not forget to return it at the end.

   /** Continue to obtain a value from the user until it is in the
    * range [lowLim, highLim].  Then return the value in range.
    * Use the specified prompt, adding a reminder of the allowed range. */
   static int InputIntInRange(string prompt, int lowLim, int highLim)
   {
      int number = InputInt(prompt);
      while (number < lowLim || number > highLim) {
         Console.WriteLine("{0} is out of range!", number);
         number = InputInt(prompt);
      }
      return number;
   }

You can try this full example, PromptUserLoop1.cs. Look at it and then try compiling and running. If you ran it without looking at the Main code, you might be confused about what values it would accept.

There are two approaches here: The caller could give a more explicit prompt. Since the limits are given as parameters, anyway, we prefer to have the program elaborate the prompt. If the limits are -10 and 10, automatically add to the prompt something like (-10 through 10).

We could use

Console.Write(" ({0} through {1}) ", lowLim, highLim);

but we need the code twice, and it is quite a mouthful. Thus far we have only seen the use of format strings when immediately printing with Console.Write (or WriteLine). Here we would like to generate a string, for use later.

We introduce the C# library function string.Format, which does just what we want: The parameters have the same form as for Console.Write, but the formatted string is returned.

Here is a revised version, in example PromptUserLoop2.cs:

   /** Continue to obtain a value from the user until it is in the
    * range [lowLim, highLim].  Then return the value in range.
    * Use the specified prompt, adding a reminder of the allowed range. */
   static int InputIntInRange(string prompt, int lowLim, int highLim)
   {
      string longPrompt = string.Format("{0} ({1} through {2}) ",
                                        prompt, lowLim, highLim);
      int number = InputInt(longPrompt);
      while (number < lowLim || number > highLim) {
         Console.WriteLine("{0} is out of range!", number);
         number = InputInt(longPrompt);
      }
      return number;
   }

The only caveat with string.Format is that there is no special function corresponding to Console.WriteLine. You can generate a newline with string.Format: Remember the escape code "\n". Put it at the end to go on to a newline.

This time around we did the user input correctly, with the request for new input repeated at the end of the loop. That repetition is easy to forget. Before we see what happens, note:

Warning

A while loop may be written so the continuation condition is always true, and the loop never stops by itself. This is an infinite loop. In practice, in many operating environments, particularly where you are geting input from the user, you can abort the execution of a program in an infinite loop by entering Ctrl-C.

In particular you get an infinite loop if you fail to get new input from the user at the end of the loop. The condition uses the bad original choice forever. Here is the mistaken version, from example PromptUserLoop2Bad.cs:

   /** Continue to obtain a value from the user until it is in the
    * range [lowLim, highLim].  Then return the value in range.
    * Use the specified prompt, adding a reminder of the allowed range. */
   static int InputIntInRange(string prompt, int lowLim, int highLim)
   {
      string longPrompt = string.Format("{0} ({1} through {2}) ",
                                        prompt, lowLim, highLim);
      int number = InputInt(longPrompt);
      while (number < lowLim || number > highLim) {
         Console.WriteLine("{0} is out of range!", number);
         // number = InputInt(longPrompt);  //comentted out new input
      }
      return number;
   }

You can run it. Remember Ctrl-C ! There are two tests in Main. If you give a legal answer immediately in the first test, it works fine (never getting into the loop body). If you give a bad input in the second test, you see that you can never fix it! Remember Ctrl-C !

A more extreme abort is to close the entire console/terminal window running the program.

6.3.1. Agree Function Exercise

Save example PrompUserLoop4Stub.cs as PromptUserLoop4.cs.

Yes-no (true/false) questions are common. How might you write an input utility function Agree? You can speed things up by considering only the first letter of responses. If it is important that the user enter correctly, you should consider three categories of answer: ones accepted as true, ones accepted as false, and ambiguous ones. You need to allow for the possibility that the user keeps giving ambiguous answers....

6.3.2. Interactive Sum Exercise

Write a program SumAll.cs that prompts the user to enter numbers, one per line, ending with a line containing 0, and keep a running sum of the numbers. Only print out the sum after all the numbers are entered (at least in your final version).

6.3.3. Safe Whole Number Input Exercise

Save example TestInputWholeStub.cs as TestInputWhole.cs that tests a function InputWhole, as described below.

There is an issue with reading in numbers with the InputInt function. If you make a typo and enter something that cannot be converted from a string to the right kind of number, a naive program will bomb. This is avoidable if you test the string and repeat if the string is illegal. Places where more complicated tests for illegality are needed are considered in Safe InputInt and InputDouble Exercise. For now we just consider reading in whole numbers (integers greater than or equal to 0). Note that such a number is written as just a sequence of digits. Follow the interactive model of InputIntInRange, looping until the user enters something that is legal: in this case, all digits.