Other Decision Making Management Tools
Using Sentinels
There are times that just counting numbers will not help your loop figure out when to start or stop. Sometimes your program will have to directly respond to a user's input, or some other changing or ending condition. For example, if you wrote a program that required users to enter a set of grades, the grade capture component might look like the following.
// in global constant area of program
private final int SENTINEL_VALUE = -1;
// user prompt
// method: printString
conIO.printString( "Enter inventory numbers, enter -1 to quit" );
// first input - loop prime
// method: promptForInt
answer = conIO.promptForInt( "Enter first value: " );
// loop until sentinel value
while( answer != SENTINEL_VALUE )
{
// add new value to sum
gradeTotal += answer;
// acquire next input value
// method: promptForInt
answer = conIO.promptForInt( "Enter next value: " );
}
The code provided shows the use of a sentinel. A special value that your program watches for to make a decision is called a sentinel. This may be in user input from the keyboard, or from an input data file, or even from some other device such as a control switch. A key point about a sentinel is that it has to be different from the data it is following. In the example above, the sentinel was a negative number because all legitimate data would naturally be positive.
In earlier days of programming, some systems like printers had to tell the difference between text being sent to the printer and any control sequences that might tell the printer to take some action like form feed or line feed, tab, carriage return, and even ring a bell. These devices would be sent a special character that was unlikely to be found in a normal stream of text, and they would be called "control characters" or "escape characters". One of the sentinels used for some printers was a right bracket (i.e., ']') with a letter immediately following it; this was highly unlikely to be found in normal text that was being printed.
Another control character is still in use in ASCII systems, and it is the backslash ('\'). Any time a backslash is placed in text data, it is followed by a letter or character, and again, this allows us to send "messages" to other systems, or sometimes other software components, that something is to be controlled. Examples are '\t' for tab, '\r' for carriage return, '\n' for newline, and yes, '\b' for bell (which may or may not work in most systems any more). You will be using a couple of these control characters at a point in the near future when you start working with file I/O and string management. For the device that is being "controlled" or "messaged", these are sentinels since the device or software is watching for them, and upon accepting them, will change some action or condition related to their operation at the time. Stay Tuned.
Using Flags
You could also implement some program management by setting a flag, but there is a slight difference in the use of a sentinel and a flag. A sentinel is almost always delivered from one device or software system to another, separate one. On the other hand, a flag is usually used for "messaging" inside a given set of code, being set in one part of the program, and controlling actions in latter parts of the same program. A flag can be a Boolean value that is changed when the program process should be modified or stopped. Consider for example the following code segment.
// in method where loop is (local)
boolean programContinue = true;
// inital prompt for input
// method: printString
conIO.printString( "Enter student grade, enter -1 to quit" );
// begin loop
do
{
// get input from user
// method: promptForInt
answer = conIO.promptForInt( "Enter first value: " );
if( answer >= 0.00 )
{
// add user input to sum
gradeTotal += answer;
}
else
{
// set flag to stop program
programContinue = false;
}
}
while( programContinue );
// end loop, with decision to continue or stop
Flags can be used in integer form as well. For example, if you have a quadratic calculation program that looks at the discriminant and sets a flag to 1 (no roots), 2 (one root), or 3 (two roots), your program can use that flag later to make decisions on what actions to take, and/or what to display.
Also note that if you do this, you are very wise to use constants for your flags (e.g., const int NO_ROOTS = 1;, const int ONE_ROOT = 2;, etc). Just using numbers as codes in a program is a guarantee of poor readability, but using self-documenting constants for this purpose is an excellent practice.
The following is an extended example of the use of integer flag values, and how it can both help control program actions, and improve the readabiliy. The program segments are provided with small amounts of commentary between them.
// in global constant area of program
const int NO_ROOTS = 1001;
const int ONE_ROOT = 2002;
const int TWO_ROOTS = 3003;
It must be noted that the numbers used for flag constants don't matter. As long as they are different from each other in each given set of flags, they will work fine. The biggest reason this will work is that you will never use the actual numbers anywhere in the program. You will only use the constant identifiers when the flags are needed (as you will see as the example continues). Note that the checkForRoots method, shown in red, is shown implemented later on this page.
// program start up actions
// some action to get three coefficients from user
// calculation of discriminant
// method: calcDiscriminant
discriminant = calcDiscriminant( coef_A, coef_B, coef_C );
// first action to check for number of roots
// method: checkForRoots
rootFlag = checkForRoots( discriminant );
// check for two roots
if( rootFlag == TWO_ROOTS || rootFlag == ONE_ROOT )
{
// find square root of discriminant
// method: sqrt
discSquareRoot = sqrt( discriminant );
// find denominator
denominator = 2 * coef_A;
// calculate first root
// method: calcRoot
rootOne = calcRoot( coef_B, discSquareRoot, denominator );
It was safe at this point to calculate the first root since this code block is within the protection of the flags that indicated there was at least one root. However, it is not known at this point if there is a second root. Thus, a second test is conducted next to decide to calculate the second root.
// check for second root
if( rootFlag == TWO_ROOTS )
{
// calculate second root
// method: calcRoot
rootTwo = calcRoot( coef_B, -discSquareRoot, denominator );
}
}
This ends the processing part of the program. Other program components could be implemented here since the flags will still be available later in the program to let other parts of the program know what actions can be implemented. Note that the displayResults method, shown in red, is also implemented later in this document.
// other parts of the program that are implemented
// display output
// method: displayResults
displayResults( rootFlag, rootOne, rootTwo );
// program ending actions
// end of main method
While much of this code could have been implemented above, the following offers an opportunity to see how a couple of the actions could be conducted in methods.
// supporting method implementations
int checkForRoots( int discrim )
{
// initialize method/variables
// none
// check for positive discriminant
if( discrim > 0 )
{
// return two roots flag
return TWO_ROOTS;
}
// otherwise, check for zero discriminant
else if( discrim == 0 )
{
// return one root flag
return ONE_ROOT;
}
// assume no roots, and return
return NO_ROOTS;
}
The next method provides an example of the use of flags in a switch statement. It also offers an opportunity to discuss the "No processing in an I/O method" rule. As you can see here, if there are decisions to be made in an I/O method, that is acceptable, and can be very helpful for selecting the correct output (as shown). The key point is that this method does not do any calculating or processing of data and/or generating any results that would be returned back to the main or calling method. All the processing implemented in this method is strictly used for managing the output process.
void displayResults( int flag, double firstRoot, double secondRoot )
{ // initialize method/variables
// none
// make selection on flag
switch( flag )
{
// case of one root or two roots
case ONE_ROOT:
// display one (first) root
// method: printString, printEndline
conIO.printString( "Root: " + firstRoot );
conIO.printEndline();
break;
// case of two roots
case TWO_ROOTS:
// display both roots
// method: printString, printEndline
conIO.printString( "Root One: " + firstRoot );
conIO.printEndline();
conIO.printString( "Root Two: " + secondRoot );
conIO.printEndline();
break;
// if one or two roots not found, assume no roots
default:
// display no roots message
// method: printString, printEndline
conIO.printString( "No roots were found "
+ "for this set of coefficients." );
conIO.printEndline();
break;
}
}
Flags and sentinels are simply tools you can use to manage your programs' branching and repeating processes. As stated, they help your program identify places where decisions need to be made. Sentinels offer information from the "outside", usually being input from somewhere until the sentinel indicates an end or a change of condition. Flags are simply little "messengers" that allow you to communicate information "within" your program so that conditions or states can be shared with other parts of the program that need the information to make decisions or protect parts of the program (e.g., as the NO_ROOTS flag did by keeping the program from attempting to find the square root of a negative number). Use them when they are appropriate and/or when they can make the program code clearer. They can be very helpful.