TOC PREV NEXT INDEX


I've just released a deduplicating backup product for
VMWare Workstation and Server.
Do you need to reduce the storage needed to maintain multiple backups?
Or do you need multiple snapshots on VMWare Server?
If you have either of these needs, just click
here to get started for only $29 (limited time offer ends July 31st, 2011)!

C++: A Dialog


CHAPTER 13 Analyzing the Home Inventory Project
In this chapter, we will analyze the final version of the home inventory program. I hope that in this process you will develop a better notion of how much work it takes to create even a relatively simple program that solves its users' problems in a natural and convenient way. We'll get started as soon as we cover some definitions and objectives.
13.13. Definitions
A cursor is an abstract object that represents the position on the screen where input or output will occur next.
The global namespace is a name for the set of identifiers that is visible to all functions without a class or namespace name being specified.

13.14. Objectives of This Chapter

By the end of this chapter you should have:
1. Learned how to use a namespace to group a number of functions together even if they don't have any associated data;
2. Learned how to write an "endless" loop to execute a section of code an indefinite number of times;
3. Learned how to check numeric input for validity and give the user a reasonable error message if that input is invalid;
4. Learned how to obtain keyboard input without having to wait for the user to hit the Enter key;
5. Learned how to write output on the screen at a particular position;
6. Learned how to clear the screen or an individual line on the screen;
7. Learned how to display part of a list on the screen, scrolling the rest of the list on and off the screen as necessary;
8. Learned how to use the xstring concatenation operator to aid in the formatting of data;
9. Learned how to compare and sort xstring data without regard for case;
10. Learned how to print data on the printer, including the use of the "form feed" character to ensure that any further data sent to the printer will start on a new page;
11. Learned how to delete an item from a list kept in a Vec.

13.15. The Final Version of the Home Inventory Program

Now let's get to the detailed analysis of the final version of the home inventory program. We'll start with the main() function of this program, which is shown in Figure 13.1.
FIGURE 13.1. The main() function of the final version of the home inventory main program (from code\hmtst8.cpp)
int main()
{
ifstream HomeInfo("home.inv");
HomeInventory MyInventory;
char option;
string answer;

MyInventory.LoadInventory(HomeInfo);

for (;;)
{
option = GetMenuChoice(MyInventory);

if (option == Exit)
break;
else
ExecuteMenuChoice(option,MyInventory);
}

HomeInfo.close();

for (;;)
{
cout << "Save the changes you have made? (Y/N) ";
cin >> answer;

if ((answer == "Y") || (answer == "y"))
{
ofstream NewHomeInfo("home.inv");
MyInventory.StoreInventory(NewHomeInfo);
cout << "Inventory updated" << endl;
break;
}
else if ((answer == "N") || (answer == "n"))
{
cout << "Changes cancelled" << endl;
break;
}
else
windos::clrscr();
}

return 0;
}

This starts off much like the previous versions, by creating a HomeInventory object called MyInventory and loading it with data from an input file called home.inv. Then it enters an "endless" for loop that calls the GetMenuChoice function to find out what operation the user wants to perform, then calls the ExecuteMenuChoice function to perform that operation.

We haven't yet used an "endless" loop, which is written as "for (;;)".1 Since we haven't specified any initialization, modification, or continuation expression, such a loop will run until it is interrupted by a break or return statement. In this case, the break statement is executed when the user indicates that he or she is finished with the program by entering the code for "exit" in the GetMenuChoice function.

Once the user is done entering, modifying, and examining data, the second "endless" for loop asks the user whether any changes should be made permanent by being written out to the home.inv file. If the user answers "y" or "Y", the changes are written out; if the answer is "n" or "N", the changes aren't written out. In either case, the "endless" loop is terminated, as is the program immediately afterward. If the user doesn't type a valid character, the program keeps asking the question until it gets an answer it likes.

So much for the bird's-eye view of the program. Now let's take a more detailed look at how it works. The first topic we'll examine is where that Exit value came from. This question is answered by the enum defined in Figure 13.2.

FIGURE 13.2. The MenuItem enum (from code\hmtst8.cpp)
enum MenuItem {AddItem=1, SelectItemFromNameList,
EditByPartialName, EditByDescription, EditByCategory,
DeleteItemFromNameList, PrintNames, PrintAll, Exit
};

As you can see, this enum lists all of the possible menu choices from which the user can select. I've put it at the top of the hmtst8.cpp source file because the values it defines are needed in more than one function in the main program, and that's the easiest way to make these values available to several functions.

Next, let's look at the GetMenuChoice function, shown in Figure 13.3.

FIGURE 13.3. The GetMenuChoice function (from code\hmtst8.cpp)
char GetMenuChoice(HomeInventory& Inventory)
{
short MenuRow;
short option;

for (;;)
{
windos::clrscr();

cout << Inventory.GetCount() << " items in database." << endl;

cout << endl;

cout << AddItem << ". Add item" << endl;

cout << SelectItemFromNameList <<
". Select item name from list" << endl;

cout << EditByPartialName <<
". Edit item by partial name" << endl;

cout << EditByDescription <<
". Edit item by description" << endl;

cout << EditByCategory <<
". Edit item by category" << endl;

cout << DeleteItemFromNameList <<
". Delete item" << endl;

cout << PrintNames <<
". Print item names" << endl;

cout << PrintAll << ". Print data base" << endl;

cout << Exit << ". Exit" << endl;

cout << endl;

cout << "Please enter a number from " <<
AddItem << " to " << Exit << ": ";

cin >> option;

if ((option >= AddItem) && (option <= Exit))
break;
else
HomeUtility::HandleError("Sorry, that's an invalid option.");
}

windos::clrscr();

return option;
}

Most of this code is pretty simple, but there are a few new twists. To start with, we're using a screen control function that we haven't seen before: windos::clrscr. This function clears the screen so that we can start writing text on it without worrying about what might already be there.2

Once the screen has been cleared, we display each of the menu choices on the screen and ask the user to type in the number of the operation to be performed. Next, we check the entered number to make sure that it is one of the legal values. If it is, we break out of the "endless" loop, clear the screen again so that the function we're going to perform has a fresh canvas to paint on, and return the number of the operation the user selected. On the other hand, if the user has typed in an invalid value, we call a utility function called HomeUtility::HandleError that notifies the user of the error; then we continue in the "endless" loop until we get a valid answer to our question.3

Susan had some questions about this function, so we discussed it.

Susan: Is this GetMenuChoice function listing the choices on the screen?
Steve: Yes.
Susan: How does it know how to put the number of the selection in front of each one?
Steve: That's how enum values are displayed by operator <<. That's why each line begins with one of the values from the MenuItem enum.
Susan: How did you know to use the clrscr function to clear the screen?
Steve: I read the documentation for the compiler.
Susan: But how did you know there even was such a function?
Steve: Because I've used it before.
Susan: Well, what if it was the first time you ever needed it?
Steve: Then I would either have to read a book (like this one) or ask somebody. It's just like learning about anything else.
Once we know which operation the user wants to perform, main calls ExecuteMenuChoice (Figure 13.4).
FIGURE 13.4. ExecuteMenuChoice (from code\hmtst8.cpp)
void ExecuteMenuChoice(char option, HomeInventory& Inventory)
{
short itemno;
ofstream Printer("lpt1");
string Name;
string Description;
string Category;

switch (option)
{
case AddItem:
{
cout << "Adding item" << endl << endl;
Inventory.AddItem();
ofstream SaveHomeInfo("home.$$$");
Inventory.StoreInventory(SaveHomeInfo);
}
break;

case SelectItemFromNameList:
cout << "Selecting item from whole inventory";
cout << endl << endl;
HomeUtility::IgnoreTillCR();
itemno = Inventory.SelectItemFromNameList();
if (itemno != -1)
{
Inventory.EditItem(itemno);
ofstream SaveHomeInfo("home.$$$");
Inventory.StoreInventory(SaveHomeInfo);
}
break;

case EditByPartialName:
cout << "Selecting item by partial name";
cout << endl << endl;
cout << "Please enter part of the name of the item\n";
cout << "(or ENTER for all items): ";
HomeUtility::IgnoreTillCR();
getline(cin,Name);
cout << endl;
itemno = Inventory.SelectItemByPartialName(Name);
if (itemno != -1)
{
Inventory.EditItem(itemno);
ofstream SaveHomeInfo("home.$$$");
Inventory.StoreInventory(SaveHomeInfo);
}
break;

case EditByDescription:
cout << "Selecting item by partial description";
cout << endl << endl;
cout << "Please enter part of the description of the item\n";
cout << "(or ENTER for all items): ";
HomeUtility::IgnoreTillCR();
getline(cin,Description);
cout << endl;

itemno =
Inventory.SelectItemFromDescriptionList(Description);

if (itemno != -1)
{
Inventory.EditItem(itemno);
ofstream SaveHomeInfo("home.$$$");
Inventory.StoreInventory(SaveHomeInfo);
}
break;

case EditByCategory:
cout << "Selecting item by partial category";
cout << endl << endl;
cout << "Please enter part or all of the category name\n";
cout << "(or ENTER for all categories): ";
HomeUtility::IgnoreTillCR();
getline(cin,Category);
cout << endl;

itemno =
Inventory.SelectItemFromCategoryList(Category);

if (itemno != -1)
{
Inventory.EditItem(itemno);
ofstream SaveHomeInfo("home.$$$");
Inventory.StoreInventory(SaveHomeInfo);
}
break;

case DeleteItemFromNameList:
cout << "Deleting item" << endl << endl;
itemno = Inventory.SelectItemFromNameList();
if (itemno != -1)
{
string Query;
cout << "Are you sure you want to delete item ";
cout << itemno + 1 << " (Y/N)? ";
cin >> Query;
if (toupper(Query[0]) == `Y')
{
Inventory.DeleteItem(itemno);
ofstream SaveHomeInfo("home.$$$");
Inventory.StoreInventory(SaveHomeInfo);
}
}
break;

case PrintNames:
Inventory.PrintNames(Printer);
break;

case PrintAll:
Inventory.PrintAll(Printer);
break;
}
}

While this function is fairly long, there's nothing terribly complex about it. After declaring some variables and initializing an ofstream called Printer to write to the printer whose name is "lpt1", the rest of the function consists of a switch statement. Each of the cases of the switch executes whatever operation the user selected during the GetMenuChoice function. Most of these are very similar, as you'll see.4

As soon as we take care of an initial question from Susan, we'll take a look at each of these cases in order.

Susan: What's a case?
Steve: It's the part of a switch statement that executes the code for one of the possibilities. Basically, the switch and case statements are like a bunch of if/else statements but easier to read and modify.
1. The AddItem case displays a message telling the user what operation is being performed and calls the AddItem member function of the Inventory object to add the item. Then the (modified) inventory is saved to a "backup" file called home.$$$. The purpose of this file is to prevent disaster should the power fail or the system crash during a lengthy editing session. Because the inventory is written out to the home.$$$ file whenever any change is made, the user can recover the work that might otherwise be lost in case of any kind of system failure. After this is done, the processing for this step is complete, so the break statement exits from the switch statement and the ExecuteMenuChoice function returns to the main program.
2. The SelectItemFromNameList case displays a message telling the user what operation is being performed and calls the SelectItemFromNameList member function of the Inventory object to determine which inventory item the user wants to edit. If the user doesn't select an item to be edited, the result of this call will be the value -1, in which case the editing step is omitted and the ExecuteMenuChoice function returns to the main program. However, if the user does select an item to be edited, the EditItem member function of the Inventory object is called with the index of that item. When that function has finished execution, the inventory is saved as in AddItem.
Susan had some more questions at this point.
Susan: Why is the result -1?
Steve: Because 0 is a valid index; remember, we're programming in C++, and 0 is the index of the first item in the inventory list.
Susan: How about using 99?<g>
Steve: What an original idea! That would work fine until we had 99 items; unfortunately, then we would have an Item100 problem.
3. The EditByPartialName case displays a message telling the user what operation is being performed, asks the user to type in part of the name of the item to be edited, and calls the SelectItemByPartialName member function of the Inventory object to determine which inventory item the user wants to edit. The rest of this function is the same as the previous case, which makes sense because the only difference in the purpose of these two sections of code is how the user selects the item.
Susan, with her keen eye for detail, spotted a small discrepancy in this section of code, which led to the following discussion.
Susan: Why is the name of the case different from the name of the function?
Steve: That's a good question. It really should be the same, but I changed the program several times and forgot to resynchronize the two names. Because it doesn't affect the functioning of the program, I'm going to leave it as is.
4. The EditByDescription case is exactly the same as the previous case, except that it asks the user for part of the description rather than the name and calls SelectItemFromDescriptionList, rather than SelectItemByPartialName, to select the item to be edited.
5. The EditByCategory case is exactly the same as the previous two, except that it asks the user for the category name, rather than the item name or description, and calls SelectItemFromCategoryList to select the item to be edited.
6. The DeleteItemFromNameList case is a bit different from the previous cases. It starts by allowing the user to select the item to be deleted from the entire inventory. Then it asks the user to confirm the deletion of that item, just to be sure that nothing gets deleted accidentally. Then it calls the DeleteItem member function of the Inventory object to do the actual deletion. Finally, it writes the changed inventory to the backup file.
Susan had a couple of comments about this case and its corresponding function.
Susan: You know, these long names look like German words: a whole bunch of words all strung together.
Steve: I do believe you're right!
Susan: Anyway, I like the confirmation. It's better to be safe rather than to accidentally delete an item when you didn't mean to.
Steve: I agree. Of course, we don't have a "mass delete" option, so an accident can't do unlimited damage, but I'd rather be safe than sorry.
7. The PrintNames and PrintAll cases are much simpler than the previous ones because they don't allow the user to select which items will be included in the printed list. The user can print either the names of all the items in the inventory or all the data for all the items. Of course, even though the program is useful without a fancier printing capability, there might be occasions where printing data for part of the inventory would be very handy. Therefore, I've added an exercise to improve these facilities.

13.16. Using a namespace to Group Utility Functions

That takes care of the main program. Now we're going to take a look at a number of functions that have something in common but really don't constitute a class, as they don't share any data. It would be possible to use a class for grouping these functions, but there's a better way: creating a namespace to hold these functions. Of course, we've been using the std namespace throughout this book, but up until now we haven't made one of our own.5 It's not terribly difficult, as you'll see. Let's start by taking a look at the HomeUtility namespace, starting with its interface, shown in Figure 13.5.
FIGURE 13.5. The HomeUtility interface (code\hmutil1.h)

//hmutil1.h

#ifndef HMUTIL1_H

#define HMUTIL1_H

namespace HomeUtility

{

bool ReadDoubleFromLine(std::istream& is, double& Result);

bool ReadLongFromLine(std::istream& is, long& Result);

bool ReadDateFromLine(std::istream& is, long& Date);

void IgnoreTillCR(std::istream& is=std::cin);

void HandleError(const xstring& Message);

short GetNumberOrEnter(bool AllowArrows=false);

void ClearRestOfScreen(short StartingRow);

short SelectItem(const Vec<short>& Number,

const Vec<xstring>& Name);

enum KeyValue {e_Return = -1, e_Up = -2, e_Down = -3};

bool CheckNumericInput(std::istream& is);

}

#endif

This file declares a number of functions that can be used anywhere, so long as you precede their names with their namespace qualifier, namely HomeUtility. But you may be wondering why we should go to the bother of putting them all in a namespace. Why not just make them global?

The Advantages of Using namespaces

The reason is to avoid polluting the global namespace. That is, it's entirely possible that another programmer might write a function called HandleError, and we want to make sure that the code we write can coexist with code that uses such common names. By creating a namespace to hold these functions that would otherwise be global, we are preventing clashes with other functions that might have the same names.

How did I decide which functions should go into this namespace rather than anywhere else? My criterion was that the function could be used in more than one other class, so that it would most reasonably belong in a commonly accessible place such as a utility namespace.

You might not be surprised that Susan had some questions about this issue. Here is our discussion.

Susan: Exactly how is a namespace different from a class? I don't get it.
Steve: They are very similar. In fact, every class actually defines a namespace for its member variables and functions. You can think of a namespace as a class with all public static member variables and functions.
Susan: Okay, but why bother with this anyway? Why not just leave everything global?
Steve: It's an ecological issue. If we create global functions, especially ones with names that other programmers might want to use, it's like dumping garbage in the ocean where it can affect others. In fact, the practice of creating global functions without a good reason is widely referred to as "polluting the global namespace", because such functions interfere with other programmers' use of the same names. Remember, there can be only one global function with a given name and parameter list, so we should create such functions only when absolutely necessary.6
Susan: OK, but why should these functions be in this namespace? How did you decide which ones should be here?
Steve: A function belongs in the utility namespace if it is used in several other classes. We certainly don't want to copy it into each of those classes: that wouldn't be very object-oriented!
Now that we know why we need the functions in the HomeUtility namespace, I should explain what the new construct bool AllowArrows=false means in a function declaration and why we need to specify it here.

Default Arguments

This is another C++ feature that we haven't seen before. It's called a default argument, and its purpose is to specify a value for an argument to a function when the user of the function doesn't supply a value for that argument. In this case, the construct "bool AllowArrows=false" specifies that if the application programmer calls GetNumberOrEnter without specifying an argument, the AllowArrows argument will be assumed to be false. On the other hand, if the application programmer calls GetNumberOrEnter with a bool argument, the AllowArrows argument will be set to the bool value supplied in the function call.

We don't have to use default arguments if we don't want to; we can achieve the same effect by writing a separate function for each possible number of arguments supplied by the calling program. To see how this alternative works, let's start with Figure 13.6, which shows an interface file that contains two overloaded functions that can be replaced by one function with a default argument.

FIGURE 13.6. Not declaring a default argument (code\nodef.h)
void Answer(int x);
void Answer();

The implementation of the first set of functions is shown in Figure 13.7.
FIGURE 13.7. Not using a default argument (code\nodef.cpp)

#include <iostream>

#include "nodef.h"

using namespace std;

void Answer(int x)

{

cout << "Here is the answer: " << x << endl;

}

void Answer()

{

cout << "Here is the answer: " << 42 << endl;

}

main()

{

Answer(10);

Answer();

}

Now how would we do this using a default argument? Figure 13.8 shows the header file for this approach.
FIGURE 13.8. Declaring a default argument (code\default.h)
void Answer(int x=42);

Figure 13.9 shows the implementation for this function:
FIGURE 13.9. Using a default argument (code\nodef.cpp)
#include <iostream>
#include "default.h"
using namespace std;

void Answer(int x)
{
cout << "Here is the answer: " << x << endl;
}

main()
{
Answer(10);
Answer();
}

To recapitulate, specifying a default argument as int x=42 for the argument list of a function is exactly equivalent to writing two overloaded functions:
1. One with no argument, having a constant value 42 for the value to be used in the function.
2. One with an int argument having no default value.
Of course, the output of the program is the same whether we use one function with a default argument or two functions with no default arguments; the difference is only in how we implement the same functionality.

13.17. The Functions of the HomeUtility namespace

Now that we've covered that new construct, let's take a look at the functions declared in the HomeUtility namespace, starting with ReadDoubleFromLine, which is shown in Figure 13.10.
FIGURE 13.10. HomeUtility::ReadDoubleFromLine (from code\hmutil1.cpp)
bool HomeUtility::ReadDoubleFromLine(istream& is, double& Result)
{
double temp;
xstring garbage;

is >> Result;

return CheckNumericInput(is);
}

This function isn't terribly complicated. It uses the standard library input operator >> to read the data from an input stream into a double called Result, calls a function named CheckNumericInput (shown in Figure 13.15 on page 935), and returns the result of that function to the user.

However, there is one interesting detail about the way this function works: the result of the input operation is not the return value of the function. Instead, the function returns true or false to indicate the success or failure of the operation, and the actual data is returned to the caller via a reference argument.

Returning More Than One Value from a Function

Why did I design it this way? Because a function can have only one return value, but in this case I need to return two separate pieces of information: whether the operation was successful, and what the value read actually was. So I decided to return the status (i.e., success or failure) as the return value of the function and use a reference argument to return the actual value read.

The next function, ReadLongFromLine, which is shown in Figure 13.10, is almost identical to the previous one, except of course for the type of data it reads, so I don't think we need to analyze it further.

FIGURE 13.11. HomeUtility::ReadLongFromLine (from code\hmutil1.cpp)

bool HomeUtility::ReadLongFromLine(istream& is, long& Result)

{

long temp;

xstring garbage;

is >> Result;

return CheckNumericInput(is);

}

Reading a Date Field

The next function is HomeUtility::ReadDateFromLine, which is shown in Figure 13.12.
FIGURE 13.12. HomeUtility::ReadDateFromLine (from code\hmutil1.cpp)
bool HomeUtility::ReadDateFromLine(istream& is, long& Date)
{
bool result = ReadLongFromLine(is, Date);

if (result == false)
return false;

if ((Date < 18000101) && (Date != 0))
{
xstring Message = "Date must be either 0 (if unknown)";
Message = Message + " or a number in the form YYYYMMDD";
Message = Message + "\n";
Message = Message + "which is on or after 18000101";
Message = Message + " (January 1st, 1800)";
HandleError(Message);
result = false;
}

return result;
}

This isn't a very complicated function either. It starts by calling ReadLongFromLine to get the "date" from the user. If that function returns false, that means the user typed in something illegal. In that case, ReadDateFromLine returns the bad news to the calling function in the form of a false return value. If the input value is a valid number, the second if statement checks whether it is in the range that I've decided to allow (after consultation with Susan): on or after January 1, 1800, or 0. The latter is needed to handle the very real possibility that Susan pointed out; the user may not know when the object was acquired. If the date fails this test, we return false to indicate that it's invalid; otherwise, the return value is true, and the actual date is set via the reference argument Date.

Even though I had discussed the range of legal dates with Susan, she had a couple of additional questions about this notion.

Susan: Why do we need a limit on the date at all?
Steve: To reduce the likelihood of an error. The chance that the user has had the object for more than a couple of hundred years is smaller than the chance that the user typed the date in incorrectly. At least, that's the way I figure it.
Susan: What about collectors who have extremely old objects?
Steve: Well, that is certainly possible. However, the field we're discussing here represents the date that the user acquired the object, not how old the object is. However, that might very well be a useful piece of information, especially for collectibles, so I'll add an exercise to include such a field in a collectibles type.
The next function we will look at is IgnoreTillCR, which is shown in Figure 13.13.
FIGURE 13.13. HomeUtility::IgnoreTillCR (from code\hmutil1.cpp)

void HomeUtility::IgnoreTillCR(istream& is)

{

is.ignore(numeric_limits<streamsize>::max(),'\n');

}

This is a simple function that just calls the ignore function of the istream class to ignore as many characters as might potentially be in cin, or up to the first newline (or "carriage return") character, whichever comes first. In fact, it's so simple that you might wonder why I even bothered making it a function.

The reason I made this a function wasn't to reduce typing but to localize the knowledge of how it works in one place. That way, if I decided to use a different mechanism to ignore excess data, I could change just this one function rather than trying to find every place that I had used the ignore function for that purpose.

Susan had some questions about this function.

Susan: When do we need this function again?
Steve: To ignore any extra characters that the user might type in before hitting the Enter key. Otherwise, those characters will be interpreted as commands after Enter is pressed.
Susan: OK, but what does numeric_limits<streamsize>::max() mean?
Steve: That's a function from the standard library that returns the maximum number of characters that could ever be in a stream. If we ignore that many characters, we can be sure that there aren't any left in the stream.
Susan: Yes, but how many characters is that?
Steve: Using the compiler on the CD in the back of the book, 2147483647. I don't suppose any user is going to type that many characters before he hits the Enter key, do you?
Next, we have HomeUtility:HandleError, which is the common error-handling function used in the rest of the program whenever we simply want to display an error message, wait for the user to hit Enter, and then continue with the program. The code for HandleError is shown in Figure 13.14.
FIGURE 13.14. HomeUtility::HandleError (from code\hmutil1.cpp)
void HomeUtility::HandleError(const xstring& Message)
{
cout << endl;
cout << Message << endl;
cin.clear();
IgnoreTillCR();
cout << "Please hit ENTER to continue." << endl;
cin.get();
}

This is a simple function too. It starts by moving the cursor to the next line on the screen (so the user can tell where the error message begins). Then it displays the message, using the cin.clear() function call to clean up any errors that might have occurred in the cin input stream and calling IgnoreTillCR() to ignore any random characters that might be left in cin from the user's last input. Next, it displays the message "Please hit Enter to continue" and waits for the user to hit a key, which is accepted via the cin.get() function call.

Susan had a couple of comments and questions about this function:

Susan: So, you have to clean up the garbage in the input stream before you can use it again?
Steve: Yes, we have to reset the status of the stream before we can read from it again. This prevents a program from continuing to read garbage from the stream without realizing it.
Susan: What error message does this function display?
Steve: Whatever error message the calling function specifies.
Susan: Oh, I see. It's generic, not specific to a particular situation.
Steve: Yes, that's exactly right.
The next utility function is HomeUtility::CheckNumericInput, which is called after the user has typed in a numeric value. The code for this function is shown in Figure 13.15.
FIGURE 13.15. HomeUtility::CheckNumericInput (from code\hmutil1.cpp)
bool HomeUtility::CheckNumericInput(istream& is)
{
int NextChar = is.peek();
if (NextChar == `\n' || NextChar == ` `)
IgnoreTillCR(is);
else
{
xstring garbage;
is.clear();
is >> garbage;
xstring Message = "Illegal data in numeric value: ";
Message = Message + garbage;
HandleError(Message);
return false;
}

return true;
}

While this is a bit more complex than the functions we've looked at so far in this namespace, it shouldn't be hard to follow. After declaring some variables, the code starts by calling the peek function to see whether the next character in the input stream is a newline (`\n') character or a space (` '). If it is, we can tell that the entry is a valid number, so we don't have to deal with an error. How do we know this?

Reading Numeric Data from a Line Oriented File

We know this because the versions of operator >> that read numeric (short, int, long, float, or double) values stop when they get to a character that doesn't belong in a number. In the current case, we've tried to read a number either from the keyboard or an input file, which is supposed to be terminated by a newline (Enter). If the data that we've read is a valid numeric value, all of the characters up to (but not including) the Enter key will already have been used by operator >> in setting the new value of the numeric variable to the right of the >>. Therefore, the next character in the input stream should be the Enter (which is represented in C and C++ by the newline character, `\n'). However, if the data includes one or more characters that don't belong in a number, the first of those characters will be the next character in the input stream after operator >> finishes reading the value for the numeric variable to its right. Therefore, if the next character isn't a newline or a space, we know the user typed in something illegal.

In that case, we should let the user know exactly what the illegal characters were. Therefore, after we call the clear function to clear the error status of the istream, the next statement, cin >> garbage, uses operator >> to read the rest of the characters in the input line into a xstring called garbage, which will end up holding everything from the first illegal character up to but not including a newline. Then we construct the whole error message by concatenating the illegal characters to the end of the message "Illegal data in numeric value: ". Finally, we call HandleError to display the message and wait for the user to press Enter.

A Tradeoff between Convenience and Error Control

If you're following this closely, you may have noticed that I'm not enforcing the rules for input very strictly here. Why do I allow a space at the end of a number when the number is supposed to be followed by a newline?

There's an interesting design issue here. Spaces are invisible, which means the error message "Illegal data in numeric value: " wouldn't be very informative to the user. And who cares if the user hits the space bar by accident after entering his numeric value?

But there's another reason that I allow a space at the end of a numeric value: this function is also used to read data from a file, not just interactively. Imagine how annoying it would be if you had to try to figure out which line in a big data file ends with a space! I decided being overly strict on this would cause more trouble than it prevented.

Susan wanted to go over this in more detail.

Susan: Let me see if I understand this. If the user typed in illegal characters, the input operation would stop at that point?
Steve: Right.
Susan: Would that cause an error message?
Steve: No, not by itself; it just sets an error condition in the input stream. It's up to us to produce the error message and that's what we're doing here.
Susan: So the garbage characters have been left in cin?
Steve: Yes. That's why we have to call clear before we can use cin again.

Single Keystroke Data Entry

The next function we're going to examine in the HomeUtility namespace, GetNumberOrEnter (Figure 13.16), is somewhat more complicated than the ones we've been looking at so far. That's because it deals with getting input from the user one keystroke at a time.
FIGURE 13.16. HomeUtility::GetNumberOrEnter (from code\hmutil1.cpp)
short HomeUtility::GetNumberOrEnter(bool AllowArrows)
{
int key;
char keychar;
short FoundItemNumber;

cout.flush();

for (;;)
{
key = windos::getkey();
keychar = key;

if (key == K_Return)
return e_Return;

if (AllowArrows)
{
if (key == K_Up)
return e_Up;
if (key == K_Down)
return e_Down;
}

if ((key < `0') || (key > `9'))
continue;

cout << keychar;
cout.flush();

FoundItemNumber = key - `0';

for (;;)
{
key = windos::getkey();
keychar = key;

if (key == K_BackSpace)
{
cout << keychar;
cout.flush();
cout << ` `;
cout << keychar;
cout.flush();
FoundItemNumber /= 10;
continue;
}

if (key == K_Return)
{
cout << keychar;
cout.flush();
return FoundItemNumber;
}

if ((key < `0') || (key > `9'))
continue;

cout << keychar;
cout.flush();

FoundItemNumber = FoundItemNumber * 10 + (key - `0');
}
}
}

The first thing to note about this function is its argument, which is a bool called AllowArrows. If you look at the definition of the interface for the HomeUtility namespace (Figure 13.5 on page 925), you'll notice that this argument has a default value, which is false. The purpose of this argument is to determine whether the up and down arrow keys will be accepted as valid inputs; if the argument is false, they will be ignored, whereas if the argument is true and the user presses one of these keys, the function will return a code indicating which one. As you'll see, accepting the arrow keys will be useful in the last function in this namespace, SelectItem.

The function starts by declaring some variables called key, keychar, and FoundItemNumber. The first of these is an int, which is a type we haven't used very much because it varies in size from one compiler to another.7 However, in this case I'm going to use an int variable to hold the return value from windos::getkey(), which is the function that returns the key code for a key that has been pressed by the user. Since windos::getkey is defined to return an int value, that's the appropriate type for a variable that holds its return value.8

This windos::getkey function is very useful because it allows us to get input from the user without having to wait for him or her to hit Enter. Under most circumstances, it's much easier to use the >> operator to get input from the user via the keyboard, but that approach has a serious limitation; it prevents us from giving the user immediate feedback. Such feedback is essential if we are going to allow the user to access varying segments of a large quantity of information in an intuitive manner, as you'll see when you try the program. Forcing the user to hit Enter before getting any feedback would be very inconvenient, and we have to worry about how easy our programs are to use if we want happy users. Therefore, I've written this GetNumberOrEnter function to allow the user to receive immediate gratification when using our home inventory program.

This might be a good time for you to try the program out for yourself so you can see what I'm talking about. First, you have to compile it by following the compilation instructions on the CD. Then type hmtst8 to run it. Try out the program for a while and see how you like it before coming back to the discussion. Pay special attention to the "select" and "edit" functions, which allow you to see some of the items in the list, and use the up and down arrows to see more of the list. That behavior is implemented partly by GetNumberOrEnter and partly by the SelectItem function.

Now that we've seen how GetNumberOrEnter is used, let's get back to its implementation. As I've already mentioned, this is a somewhat complicated function, because it has to deal with the intricacies of reading data from the keyboard one character at a time. First, we call flush to make sure that any characters that have been written to cout have actually been sent to the screen. Then we start the "endless" loop that will allow the user to type as many keys as necessary to enter the data item. Why do I say "data item" rather than "number"? Because the user can type keys that aren't numbers at all, including the up or down arrow to select a different position in the list of items that appears on the screen.

Susan had a question about the flush function.

Susan: I don't remember seeing this flush function before. What does it do?
Steve: It makes sure that everything we were planning to display on the screen is written out before we ask for any input from the user. Ordinarily, characters that are written to a stream are not sent immediately to the output device to which the stream is attached, because that is extremely inefficient. Instead, the characters are collected in an output buffer until there are enough of them to be worth sending out; this technique is called buffering.9 However, in this case we have to make sure that any characters that were supposed to be displayed have been displayed already, because we are going to be taking characters from the user and displaying them immediately. Any leftover characters would just confuse the user.
Now let's continue with the analysis of the code in GetNumberOrEnter. The first statement inside the "endless" loop is key = windos::getkey();. This statement calls a function called windos::getkey, which waits for the user to type a key and then reads it. The return value of getkey is the ASCII code for the key the user hit. Therefore, that statement should be relatively simple to understand.

Echoing the Typed Character to the Screen

However, the same is not true of the statement keychar = key;. Why would we want to assign one variable the same value as that of another? Because of the way that getkey works. Unlike normal cin input, getkey input is "silent"; that is, keys that are pressed by the user do not produce any visible results by themselves, so we have to display each character on the screen as the user types it. But to display a character on the screen via cout, the variable or expression to be displayed must have the type char, whereas the variable key is an int. If we were to write the statement, cout << key;, the program would display the ASCII numeric value for the key the user pressed. Needless to say, this would not be what the user expected; therefore, we have to copy the key value from an int variable to a char variable before we display it so that the user sees something intelligible on the screen.

Susan thought such cryptic output might have some use. Also, she wanted to know what would happen if the key value wouldn't fit in a char.

Susan: If we displayed the ASCII value instead of the character, it would be sort of a secret code, wouldn't it?
Steve: Sort of, although not a very secure one. However, it would be fairly effective at confusing the user, which wouldn't be good.
Susan: OK. Now, about copying the value from an int to a char: a char is smaller than an int, right? What if the value was too big?
Steve: The answer is that the part that wouldn't fit would be chopped off. However, in this case we're safe because we know that any key the user types will fit in a char.
The next order of business in this function is to check whether the user has hit the Enter key. If so, we simply return the enum value e_Return to the calling function to inform it that the user has hit the Enter key without typing a value.

Assuming that the user has not hit the Enter key so far, we check the AllowArrows argument to see whether the arrow keys are allowed at this time. If they are, we check to see if either the up arrow or the down arrow has been hit. If it has, we return the appropriate code to tell the calling function that this has occurred so it can scroll the display if necessary.

The next statement after the end of the arrow-handling code is an if statement that checks whether the key that we are handling is in the range `0' to `9'. If the key is outside that range, we use the continue statement to skip back to the beginning of the outer for loop, essentially ignoring any such key. However, if the key is within the numeric digit range, we proceed by using the operator << to send it to the screen. Then we use the flush function of the cout object to ensure that the key has actually been displayed on the screen.

Susan wanted to know why we had to worry about whether the user typed a valid key:

Susan: Why do we have to worry about the user typing in the wrong key? Can't the user take some personal responsibility for typing in the data correctly?
Steve: Anyone can make a mistake, especially when typing in a lot of information in a row. How would you like it if programs would let you type in any kind of garbage without telling you what you are doing was illegal? For example, if you were trying to list something on an internet auction site and you typed the price in as "$1x", wouldn't you want the auction listing program to tell you that was invalid?
By this point, we have seen the first digit of the value, so we continue by setting FoundItemNumber to the numeric value of that digit, which can be calculated as the ASCII value of the key minus the ASCII value of `0'.

Getting the Rest of the Numeric Value

Now we're ready to enter the inner for loop that gathers all the rest of the numeric digits of the number. This starts with the same "endless" condition, (;;), as the outer loop because we don't know how many times it will have to be executed. Therefore, rather than specify the loop count in the for statement, we use a return statement to exit from the loop (and the function) as soon as the user presses Enter.

The first two statements in this inner loop are exactly the same as the first two statements in the outer loop, which should not be a surprise as they serve exactly the same function - to get the key from the user and copy it into a char variable for display later. However, the next segment of code is different, because once the user has typed at least one digit, another possibility opens up - editing the value by using the backspace key to erase an erroneous digit. That's the task of the next part of the code, which was a bit more difficult to develop than you might think. The problem is that simply echoing the backspace key to the screen, as we do with other keys, does not work properly because it leaves the erroneous digit visible on the screen.

Susan had a comment about the effect of hitting backspace:

Susan: Every time I hit backspace in a program, it erases what I've just typed in. So why do you say it doesn't?
Steve: Programs have to be written specifically to take care of this issue. Obviously, you've never seen one that doesn't do it correctly, which isn't too surprising, because that sort of error should be caught very early in the development of a program.
Even after we solve this problem, however - by writing a space character on the screen to erase the erroneous digit and backing up again to position the cursor at the correct place for entering the new digit - we have another problem to deal with. Namely, we have to correct the value of the FoundItemNumber variable to account for the erased digit. This requires only that we divide the previous value of that variable by 10 because the remainder will be discarded automatically by the integer division process, effectively eliminating the contribution of the erased digit. Once we have taken care of these details, we are finished with this particular keystroke, so we use a continue statement to proceed to the next execution of the loop.

Susan had a question about this part of the processing also:

Susan: I don't understand what you're doing with the digit values here.
Steve: Well, whenever the user types in a new digit, we have to recompute the value of the number that is being entered. For example, if a 3 is the first digit, the value so far is three. If the second digit is 2, then the total value so far is 32. But how do we calculate that? By multiplying the previous value entered by 10 before adding the new digit. So once the 2 is entered, we multiplying the previously existing value (3) by 10, giving the value 30. Then we add the value of the new digit, 2, giving the value 32.
So what happens if the user hits the backspace key? After erasing the latest digit, we need to correct the stored value of the number being entered so it will correspond to what is on the screen. To do this, all we have to do is divide the current value by 10, as that will effectively eliminate the last digit of the value.
The next possibility to be handled is that of the Enter key. When we see that key we display it on the screen, which of course causes the cursor to move to the next line. Then we return the value of the FoundItemNumber variable to the calling function, which ends the execution of this function.

By this point in the function, we shouldn't be seeing anything but a digit key. Therefore, any key other than a digit is ignored, as we use the continue statement to skip further processing of such a key.

We're almost done. The last phase of processing is to display the digit key we have received and use it to modify the previous value of the FoundItemNumber variable. The new value of the FoundItemNumber is 10 times the previous value plus the value of the new digit, and that's exactly how the last statement in this function calculates the new value.

Clearing Part of the Screen

I'm sure you'll be happy to hear that the next function we will discuss is a lot simpler than the one we just looked at. This is the ClearRestOfScreen function, which is shown in Figure 13.17. It is used in the final function in the HomeUtility namespace, SelectItem, to clear the part of the screen that function uses for its item display.
FIGURE 13.17. HomeUtility::ClearRestOfScreen (from code\hmutil1.cpp)
void HomeUtility::ClearRestOfScreen(short StartingRow)
{
short i;

short HighestRowNumber = windos::ScreenRows();

for (i = StartingRow; i <= HighestRowNumber; i ++)
{
gotoxy(1,i);
clreol();
}

gotoxy(1,StartingRow);
}

This is the first function we've seen that uses a couple of screen-handling functions from the conio library, gotoxy and clreol.10 The first of these functions moves the cursor to the column (X) and row (Y) specified by its arguments. The first argument is the column number, which for some reason doesn't follow the standard C and C++ convention of starting with 0 but starts at 1. The same is true of the row number, which is the second argument to the function.

The second conio library function that we haven't seen before is the clreol function, which erases everything on a given line of the screen from the cursor position to the end of the line. We call this function for each line from StartingRow to the end of the screen.

Before we can clear the screen one line at a time, however, we need to know when to stop. That's why we need to call the other non-standard library function in this function: windos::ScreenRows. As its name suggests, it returns the number of rows on the screen that we can use for displaying data.11

Susan had a few questions about this function.

Susan: What is conio?
Steve: It stands for "console I/O". Before PCs, it was common for programmers to use a terminal that consisted of a video display screen and a keyboard; this combination was referred to as a "console".
Susan: How do you pronounce gotoxy?
Steve: It's pronounced "go-to-X-Y".
Now that we've seen how ClearRestOfScreen works, I should tell you why we need it: to allow the SelectItem function to keep its working area clear of random characters. Of course, it could be used in other situations, but that's how we're using it here.

The Implementation of HomeUtility::SelectItem

The final function in the HomeUtility namespace is SelectItem, whose code is shown in Figure 13.18.
FIGURE 13.18. The HomeUtility::SelectItem function (from code\hmutil1.cpp)
short HomeUtility::SelectItem(const Vec<short>& Number,
const Vec<xstring>& Name)
{
short FoundItemNumber;
int Row;
int Column;
int RowCount = windos::ScreenRows();
short ItemCount = Name.size();

windos::ScreenGetCursor(Row,Column);
Row ++;

// Max number of rows in scroll area is 1/2 available rows on screen

int RowsAvail = RowCount / 2;

if (RowsAvail > ItemCount)
RowsAvail = ItemCount;

if (RowsAvail == 0)
{
HandleError("No items found.");
return 0;
}

short offset = 0;
for (;;)
{
ClearRestOfScreen(Row);

for (short i = offset; i < offset + RowsAvail ; i++)
cout << setw(5) << Number[i] + 1 << ". " << Name[i] << endl;

cout << endl;

cout << "Type item number to select or ENTER to end." << endl;

if (ItemCount > RowsAvail)
cout << "Hit down arrow or up arrow to scroll." << endl;

cout << endl;

FoundItemNumber = GetNumberOrEnter(true);
if (FoundItemNumber == e_Return)
return 0;

if (FoundItemNumber == e_Up)
{
if (ItemCount > RowsAvail)
{
offset --;
if (offset < 0)
offset = 0;
}
continue;
}

if (FoundItemNumber == e_Down)
{
if (ItemCount > RowsAvail)
{
offset ++;
if (offset >= (int)(Name.size()-RowsAvail))
offset = Name.size()-RowsAvail;
}
continue;
}

for (short i = 0; i < ItemCount; i ++)
{
if (FoundItemNumber == Number[i]+1)
return FoundItemNumber;
}

IgnoreTillCR();
cout << FoundItemNumber <<
" is an invalid entry. Hit ENTER to continue." << endl;
IgnoreTillCR();
return 0;
}
}

This function, as its name indicates, is the heart of the item selection process. Its arguments are the Number Vec, which contains the indexes into the inventory list of the particular items from which the user is selecting, and the Name Vec, which contains the names of these items and sometimes other information about them (e.g., the category in which each item is found).

The first operation to be performed in this function is determining how many lines there are on the "screen"; I have put the word "screen" in quotes because what we are actually concerned with is only the console window in which our program is running, not the actual physical screen of the monitor. The reason that the number of lines on the screen is important is that we may want to calculate the number of items to be displayed in the "scroll area" (the area where we will be displaying all or part of the list of items) based on the amount of space available on the screen.

Susan had some questions about the way we're handling the screen.

Susan: What's the difference between the monitor and the console window?
Steve: The monitor is the physical device that displays text and graphics. The console window is the virtual device that our programs run in. It's the window that says "Command prompt" at the top.
Susan: So, you even have to tell the program how big the screen is? Doesn't it know anything?
Steve: You have to realize that the same program may run on different machines that are set up differently. Even if everyone were running the same operating system, some people have their console windows set for 25 lines, some for 50 lines, and possibly other numbers of lines as well. It's not very difficult to handle all these different possibilities just by changing the return value of the ScreenRows function.
The next operation is to determine the number of entries in the list of items we're going to display, which we can do by calling the size member function of the Name argument. Of course, we could just as well call the size member function of Number, because that argument has to have the same number of elements as Name has; if they have different numbers of elements, the calling function has made a serious error!

After finding out how many items we are going to handle, the next operation is to determine the current position of the cursor so that we can position the "scroll area" properly below the heading that was displayed by the calling function. To find the current position of the cursor, we call another non-standard library function, windos::ScreenGetCursor. This function requires two arguments, both of which are references to variables. The first argument is a reference to a variable (in this case Row) that will receive the current row number. The second is a reference to a variable (in this case Column) that will receive the current column number. As soon as we have determined the current cursor row, we increment the Row variable to skip a row between the heading and the beginning of the scroll area.

The next segment of code figures out how many items to display at one time; the current version of this function, as I've already mentioned, ignores this calculation and sets the number of items to 5. This makes my (and Susan's) testing more effective because most of the complexity of this routine is in the code that deals with the possibility of having to scroll the items onto and off the screen. This code is used only when there are more items than will fit in the limited space allocated for listing them, so if we have fewer items than will fit on the screen, the code that scrolls the list is not used. As I've mentioned before, code that is never used is never tested and therefore must be assumed not to work.

Once we have decided how many items to list at once and have assigned the appropriate value to the RowsAvail variable, we then check whether the number of items we can display (as specified in that variable) is greater than the total number of items we need to display (as specified by the ItemCount variable). If this is the case, we set the total number to be displayed to the latter value.

If there are no items to be displayed, the user has requested a set of items that doesn't exist, so we call the HandleError routine to tell the user about this situation, and return to the calling function.

Assuming that we have some items to display, we are ready to start displaying them. First, we initialize a variable called offset, which keeps track of what part of the whole list is currently being displayed in the scroll area. It begins at 0 because we start by displaying the first portion of the list, which consists of the number of items that will fit in the scroll area. Of course, if all of the items fit in the scroll area, they will all be displayed.

Once we have initialized offset, we enter the "endless" for  loop that displays the elements and asks the user for input until he or she selects something. This for loop begins by calling the ClearRestOfScreen function to clear everything on the screen beyond the current cursor location.

The next two lines of code constitute the for loop that displays the items that are currently in the scroll area. The formatting of this display is somewhat interesting, at least to me, because it took me several tries to get it right. The elements of the display line include the item number, which is one more than the index of the item being displayed (to account for the first index being 0) and the item name. Initially, I simply accepted the default formatting for the item number and the name. However, as soon as the item numbers exceeded 9, I discovered that the names no longer lined up properly because the extra character in the two-digit item number pushed the name over one extra position. To solve this problem, I decided to use the setw manipulator to force the size of the item number to five digits; because the item number is a short, the program is limited to 32767 items, so five digits will be sufficient.

Susan was surprised to hear that I had overlooked this, which led to the following exchange.

Susan: I thought you left bugs in the program on purpose so I would find them.
Steve: No, as a matter of fact, I thought it was working every time I gave it to you to test (all five times). I suppose that illustrates the eternal optimism of software developers!
After displaying the items along with their corresponding item numbers, one on each line, we display the message "Type item number to select or Enter to end." Then, if we have more items than will fit in the scroll area, we display another message telling the user about the availability of the up and down arrow keys for scrolling.

Next, we call the GetNumberOrEnter function to get the item number from the user. Note that the argument to that function is true, which means that we want the function to accept the up and down arrows so that the user can hit those keys to tell us to scroll the item list either up or down. Of course, if all the items fit in the scroll area, hitting either of those keys will have no visible effect on the list.

Once we have received the value returned by the GetNumberOrEnter function, we examine it. If it is the value e_Return, the user has decided not to select any of the items. Therefore, we return the value 0 to indicate this situation.

However, assuming that the user hit something more than just the Enter key, we have to check what the exact return value was. If it was the value e_Up or e_Down, and if we have more items than will fit in the scroll area, we change the value of the offset variable accordingly. Of course, we have to be careful not to try to display items whose indexes are before the beginning or beyond the end of the Name Vec; the code that handles both of the arrow keys ensures this doesn't happen.

Finally, if we get past the handling of the arrow keys without returning to the calling function, we must have gotten a numeric value from the user. Therefore, we check that the value the user typed is actually an entry in the Number Vec. Assuming that this is the case, we return that value to the calling function.

However, if the user entered an item number that is not found in the Number Vec, we create an error message and display it. Finally, we return the value 0 to indicate to the calling function that the user did not make a valid selection.

13.18. Checking the Inventory

That concludes our tour of the HomeUtility namespace. Now it's time to look at the changes to the next class, HomeInventory. We'll start with the latest version of the header file, hmin8.cpp, shown in Figure 13.19.
FIGURE 13.19. The latest header file for the HomeInventory class (code\hmin8.h)
//hmin8.h

class HomeInventory
{
public:
HomeInventory();

short LoadInventory(std::ifstream& is);
void DumpInventory();
HomeItem AddItem();
HomeItem EditItem(short Index);
Vec<short> LocateItemByDescription(const xstring& Partial);

Vec<short> LocateItemByCategory(const xstring& Partial);
Vec<short> LocateItemByPartialName(const xstring& Partial);
void PrintNames(std::ostream &os);
void PrintAll(std::ostream &os);
void StoreInventory(std::ofstream& ofs);
void DisplayItem(short Index);

void SortInventoryByName();
short GetCount();
short SelectItemByPartialName(const xstring& Partial);
short SelectItemFromNameList();
short SelectItemFromDescriptionList(const xstring& Partial);
short SelectItemFromCategoryList(const xstring& Partial);
void DeleteItem(short Index);

private:
Vec<HomeItem> m_Home;
};

As you will see if you compare this version of the HomeInventory class interface to the previous one we examined (hmin6.h in Figure 12.23), I've deleted three functions from this interface - namely, FindItemByDescription, FindItemByName, and LocateItemByName. The first of these is no longer used in the application program, which instead uses the logically equivalent LocateItemByDescription. The other two functions are no longer necessary because they have been superseded by the new LocateItemByPartialName, which can do everything that the old functions could do and a lot more besides.

This new version of the HomeInventory class also includes changes to existing functions. Let's take them in order of their appearance in the header file, starting with the LoadInventory function. The only difference between this version and the previous one is that the new version sorts the inventory by calling the new SortInventoryByName function after loading it. I'll provide a brief explanation of how the sort function works when we get to it. I haven't bothered to reproduce the LoadInventory function here just to show you the one added line.

The next function that was changed is the AddItem function, whose new implementation is shown in Figure 13.20.

FIGURE 13.20. The latest version of HomeInventory::AddItem (from code\hmin8.cpp)
HomeItem HomeInventory::AddItem()
{
HomeItem TempItem = HomeItem::NewItem();

if (TempItem.IsNull())
return TempItem;

short OldCount = m_Home.size();

m_Home.resize(OldCount + 1);

m_Home[OldCount] = TempItem;

SortInventoryByName();

return TempItem;
}

As you can see, this version of the function checks whether the newly created item is null, using the new IsNull member function of the HomeItem class. If that turns out to be the case, it returns that null item to the calling function rather than adding it to the inventory. This new version also sorts the inventory after adding an item, just as the new version of the LoadInventory function does.

Now we're up to the EditItem function, the new version of which is shown in Figure 13.21.

FIGURE 13.21. The latest version of HomeInventory::EditItem (from code\hmin8.cpp)
HomeItem HomeInventory::EditItem(short Index)
{
bool NameChanged = false;

HomeItem TempItem = m_Home[Index];

TempItem.Edit();

if (TempItem.GetName() != m_Home[Index].GetName())
NameChanged = true;

m_Home[Index] = TempItem;

if (NameChanged)
SortInventoryByName();

return TempItem;
}

The main difference between this version of EditItem and the previous version is that this one checks to see whether the name of the item has changed. If so, EditItem calls the SortInventoryByName function to ensure that the inventory list is still sorted by the names of the items.

The next function we'll examine is LocateItemByDescription, whose new implementation is shown in Figure 13.22.

FIGURE 13.22. The latest version of HomeInventory::LocateItemByDescription (from code\hmin8.cpp)
Vec<short> HomeInventory::LocateItemByDescription(
const xstring& Partial)
{
short ItemCount = m_Home.size();
xstring Description;
short FoundCount = 0;

for (short i = 0; i < ItemCount; i ++)
{
Description = m_Home[i].GetDescription();
if (Description.find_nocase(Partial) >= 0)
FoundCount ++;
}

Vec<short> Found(FoundCount);

FoundCount = 0;

for (short i = 0; i < ItemCount; i ++)
{
Description = m_Home[i].GetDescription();
if (Description.find_nocase(Partial) >= 0)
Found[FoundCount++] = i;
}

return Found;
}

This function is quite different from its previous incarnation; even its interface has changed. That's because it now locates all the items that match the description specified in its argument, not just the first one. Therefore, it must return a Vec of indexes rather than only one. Also, because we don't know how many items will be found before we look through the list, we don't know how large the result Vec will be on our first pass. I've solved that by using two passes, with the first pass devoted to finding the number of matching items and the second pass devoted to storing the indexes of those items in the result Vec. One other construct that we haven't seen before is the use of the post-increment operator ++ inside another expression, in the statement Found[FoundCount++] = i;. When this operator is used inside another expression, the value it returns is the pre-incremented value of the variable being incremented. In this case, the value of the expression FoundCount++ is the value that the variable FoundCount had before being incremented. After that value is used, the variable is incremented so that it will be greater by one the next time it is referred to.

Besides modifying the previously noted functions, I've also added quite a few functions to this interface to implement all the new facilities this new version of the program provides. Let's take them one at a time, starting with LocateItemByCategory, which is shown in Figure 13.23.

FIGURE 13.23. HomeInventory::LocateItemByCategory (from code\hmin8.cpp)
Vec<short> HomeInventory::LocateItemByCategory(
const xstring& Partial)
{
short ItemCount = m_Home.size();
xstring Category;
short FoundCount = 0;

for (short i = 0; i < ItemCount; i ++)
{
Category = m_Home[i].GetCategory();
if (Category.find_nocase(Partial) >= 0)
FoundCount ++;
}

Vec<short> Found(FoundCount);

FoundCount = 0;

for (short i = 0; i < ItemCount; i ++)
{
Category = m_Home[i].GetCategory();
if (Category.find_nocase(Partial) >= 0)
Found[FoundCount++] = i;
}

return Found;
}

As you can see, this is almost identical to the function we've just examined, LocateItemByDescription. The only difference is that we're searching for items whose category matches the user's specification rather than items whose description matches that specification.

I'm not going to waste space by reproducing the code for the LocateItemByPartialName function, which is again almost identical to the two functions we've just looked at. The difference, of course, is that the field it examines for a match is the item's name rather than its description or category.

The next function we will examine is PrintNames, which is shown in Figure 13.24.

FIGURE 13.24. The HomeInventory::PrintNames function (from code\hmin8.cpp)
void HomeInventory::PrintNames(ostream& os)
{
short ItemCount = m_Home.size();

for (short i = 0; i < ItemCount; i ++)
{
os << m_Home[i].GetName() << endl;
}

os << `\f' << endl;
os.flush();
}

This function merely steps through all the items in the inventory and sends the name of each one to the output stream. One minor point of interest is that to ensure that any further data sent to the printer starts on a new page, I've added a "form-feed" character, represented as '\f', to the end of the output data. After sending that character to the printer, the function ends with a call to the flush function of the ostream object we are sending the data to.

Susan had a couple of questions about this function.

Susan: What is a form-feed?
Steve: It is a character that makes the printer go to a new page. It's called that because years ago printers used continuous-form paper. When you finished printing on one form, you had to send a "form-feed" character to the printer so that it would advance the paper to the beginning of the next form. Today, most printers use cut-sheet paper, but the name has stuck.
Susan: How do we know that the form-feed character has been sent to the printer? Isn't it buffered?
Steve: That's exactly why we have to call the flush function, which ensures that the form-feed has actually been sent to the printer.
The next function is PrintAll. This, as shown in Figure 13.25, is exactly like the previous function, except that it displays all the data for each item rather than just its name.
FIGURE 13.25. The HomeInventory::PrintAll function (from code\hmin8.cpp)
void HomeInventory::PrintAll(ostream& os)
{
short ItemCount = m_Home.size();

for (short i = 0; i < ItemCount; i ++)
{
os << m_Home[i] << endl;
}

os << `\f' << endl;
os.flush();
}

Now we're up to the StoreInventory function, whose code is shown in Figure 13.26.
FIGURE 13.26. The HomeInventory::StoreInventory function (from code\hmin8.cpp)
void HomeInventory::StoreInventory(ofstream& ofs)
{
short i;
short ElementCount = m_Home.size();

ofs << ElementCount << endl << endl;

for (i = 0; i < ElementCount; i ++)
{
ofs << m_Home[i];
ofs << endl;
}
}

As you can see from the code in this function, it is almost identical to the code for PrintAll. The main differences are:
1. StoreInventory writes the number of items to the file before starting to write the items (so that we can tell how many items are in the file when we read it back later).
2. It doesn't write a form-feed character to the file after all the items are written because we aren't printing the information.
The similarity between this function and PrintAll shouldn't come as too much of a surprise. After all, storing the inventory data is almost the same as printing it out; both of these operations take data currently stored in objects in memory and transfer it to an output device. The iostream classes are designed to allow us to concentrate on the input or output task to be performed rather than on the details of the output device on which the data is to be written, so the operations needed to write data to a file can be very similar to the operations needed to write data to the printer.

Susan had some questions about this function.

Susan: What is ofs?
Steve: It stands for "output file stream", because we are writing the data for the items to a file via an ofstream object.
Susan: Why is it good that writing data to a file is like writing data to the printer?
Steve: This characteristic of C++, called device independence, makes it easier to write programs that use a number of different types of output (or input) device, as they all look more or less the same. Having to treat every device differently is a major annoyance to the programmer in languages that don't support device independence.
By the way, we've also made use of this when reading data from either cin or another input stream attached to a file.
Now let's take a look at the next function, DisplayItem, whose code is shown in Figure 13.27.
FIGURE 13.27. The HomeInventory::DisplayItem function (from code\hmin8.cpp)
void HomeInventory::DisplayItem(short Index)
{
m_Home[Index].FormattedDisplay(cout);
}

This is quite a simple function, as it calls the FormattedDisplay function of the HomeItem class to do all the work of displaying the data for a particular item in the inventory. As you can see, this function always writes the data to the screen.

Sorting the Inventory

The next function we will look at is SortInventoryByName, whose code is shown in Figure 13.28.
FIGURE 13.28. The HomeInventory::SortInventoryByName function (from code\hmin8.cpp)
void HomeInventory::SortInventoryByName()
{
short ItemCount = m_Home.size();
Vec<HomeItem> m_HomeTemp = m_Home;
Vec<xstring> Name(ItemCount);
xstring HighestName = "zzzzzzzz";
xstring FirstName;
short FirstIndex;

for (int i = 0; i < ItemCount; i ++)
Name[i] = m_Home[i].GetName();

for (int i = 0; i < ItemCount; i ++)
{
FirstName = HighestName;
FirstIndex = 0;
for (int k = 0; k < ItemCount; k ++)
{
if (Name[k].less_nocase(FirstName))
{
FirstName = Name[k];
FirstIndex = k;
}
}
m_HomeTemp[i] = m_Home[FirstIndex];
Name[FirstIndex] = HighestName;
}

m_Home = m_HomeTemp;
}

I won't go into detail on the "selection sort" algorithm used in this function because I've already explained it in gory detail in Chapters 4 and 8. The only difference between this implementation and those previous versions is that here we're using the less_nocase function rather than operator < to compare the xstring variables so that we can sort without regard to case. Briefly, the basic idea of this algorithm is that we go through the inventory looking for the item that has the "lowest" name (i.e., the name that would be earliest in the dictionary). When we find it, we copy it to an output list, and then mark it so that we won't pick it again. Then we repeat this process for each item in the original list of names. This is not a particularly efficient sorting algorithm, but it is sufficient for our purposes here.

The next function, GetCount, is extremely simple. Its sole purpose is to return the number of items in the inventory so that the main program can display this information on the screen, and its implementation consists of returning the value obtained from the size member function of the inventory object. Therefore, I won't waste space reproducing it here.

The next function in the header file, SelectItemByPartialName, is more interesting. Take a look at its implementation, which is shown in Figure 13.29.

FIGURE 13.29. The HomeInventory::SelectItemByPartialName function (from code\hmin8.cpp)
short HomeInventory::SelectItemByPartialName(
const xstring& Partial)
{
Vec<short> Found = LocateItemByPartialName(Partial);

Vec<xstring> Name(Found.size());

for (unsigned i = 0; i < Found.size(); i ++)
Name[i] = m_Home[Found[i]].GetName();

short Result = HomeUtility::SelectItem(Found,Name) - 1;

return Result;
}

This function starts by using the LocateItemByPartialName function to get a list of all the items whose names match the xstring specified by the calling function in the argument called Partial (the xstring the user typed to select the items to be listed). Once LocateItemByPartialName returns the Vec of matching item indexes, SelectItemByPartialName continues by extracting the names of those items and putting them in another Vec called Name. Once the names and indexes have been gathered, we're ready to call HomeUtility::SelectItem, which will take care of the actual user interaction needed to find out which item the user really wants to edit. The result of the SelectItem function is either 0 (meaning the user didn't select anything) or an item number, which starts at 1; however, the result of the SelectItemByPartialName function is an index into the inventory list, which is zero-based, as usual in C++. Therefore, we have to subtract 1 from the result of the SelectItem function before returning it as the index into the inventory list.12

By this point, Susan had become absorbed in the role of software developer, if the following exchange is any indication:

Susan: Why are we coddling the users? Let them start counting at 0 like we have to.
Steve: The users are our customers, and they will be a lot happier (and likely to buy more products from us) if we treat them well.
But if you keep that attitude, you may very well qualify to work at certain software companies (whose names I won't mention to avoid the likelihood of lawsuits)!
The next function we'll look at is SelectItemFromNameList, whose code is shown in Figure 13.30.
FIGURE 13.30. The HomeInventory::SelectItemFromNameList function (from code\hmin8.cpp)
short HomeInventory::SelectItemFromNameList()
{
short ItemCount = m_Home.size();

Vec<short> Found(ItemCount);

for (int i = 0; i < ItemCount; i ++)
Found[i] = i;

Vec<xstring> Name(Found.size());

for (unsigned i = 0; i < Found.size(); i ++)
Name[i] = m_Home[i].GetName();

short Result = HomeUtility::SelectItem(Found,Name) - 1;

return Result;
}

This is very similar to the previous function, except that it allows the user to choose from the entire inventory, as there is no selection expression to reduce the number of items to be displayed. Therefore, instead of calling a function to determine which items should be included in the list that the user will pick from, this function makes a list of all of the indexes and item names in the inventory, then calls the SelectItem function to allow the user to pick an item from the whole inventory list.

The next member function listed in the hmin8.h header file is SelectItemFromDescriptionList. I won't reproduce it here because it is virtually identical to the SelectItemByPartialName function, except of course that it uses the description field rather than the item name field to determine which items will end up in the list the user selects from. This means that it calls LocateItemByDescription to find items, rather than LocateItemByPartialName, which the SelectItemByPartialName function uses for that same purpose.

Selecting by Category

The next function in the header file, SelectItemFromCategoryList (Figure 13.31), is more interesting, if only because it does some relatively fancy formatting to get its display to line up properly, using concatenation to append one xstring to another.
FIGURE 13.31. The HomeInventory::SelectItemFromCategoryList function (from code\hmin8.cpp)
short HomeInventory::SelectItemFromCategoryList(
const xstring& Partial)
{
Vec<short> Found = LocateItemByCategory(Partial);

Vec<xstring> Name(Found.size());
Vec<xstring> Category(Found.size());
xstring Padding;
unsigned PaddingLength;
const unsigned ItemNumberLength = 7;

unsigned MaxLength = 0;

for (unsigned i = 0; i < Found.size(); i ++)
{
Category[i] = m_Home[Found[i]].GetCategory();
Name[i] = m_Home[Found[i]].GetName();
if (Name[i].size() > MaxLength)
MaxLength = Name[i].size();
}

for (unsigned i = 0; i < Found.size(); i ++)
{
PaddingLength = MaxLength - Name[i].size();
Padding = xstring(PaddingLength);
Name[i] = Name[i] + Padding + " " + Category[i];
}

MaxLength += ItemNumberLength;
xstring Heading = "Item # Name";
unsigned HeadingLength = Heading.size();
if (MaxLength > HeadingLength)
PaddingLength = MaxLength - HeadingLength;
else
PaddingLength = 0;
Padding = xstring(PaddingLength);
Heading = Heading + Padding + " Category";
cout << Heading << endl << endl;

short Result = HomeUtility::SelectItem(Found,Name) - 1;

return Result;
}

This function starts out pretty much like the other "select" functions - calling a "locate" function to gather indexes of items that match a particular criterion, which in this case is the category of the item. However, once these indexes have been gathered, instead of simply collecting the names of the items into a Vec, we also must determine the length of the longest name, so that when we display the category of each item after its name, the category names will line up evenly. To make this possible, we have to "pad" the shorter names to the same length as the longest name. The code to do this is in the two lines of the for loop that gathers the names and categories:

if (Name[i].GetLength() > MaxLength)

MaxLength = Name[i].GetLength();

If the current name is longer than the longest name so far, we update that MaxLength variable to the length of the current name. By the time we reach the end of the list of names, MaxLength will be the length of the longest name.

In the next for loop, we calculate the amount of padding each name will require, based on the difference between its length and the length of the longest name. Then we use the xstring(unsigned) constructor to create a xstring consisting of the number of spaces that will make the current name as long as the longest name. As soon as we have done that, we add the padding and the category name to the name of each object. At the end of this loop, we are finished with the preparation of the data that will be used in the SelectItem function.

However, we still have more work to do before we call that function because we want to display a heading on the screen to tell the user what he or she is looking at. That's the task of the next section of the code. It starts out by adding the unsigned const value ItemNumberLength to the MaxLength variable to account for the length of the item number field.13

Next, we start constructing the heading line, starting with the literal value "Item #  Name". To make the category heading line up over the category names in the display, we have to pad the heading line to the length of the longest name, if necessary. This will be needed if the heading is shorter than the length of the longest name plus the allowance of ItemNumberLength characters for the item number.14 Once we have calculated the length of that padding (if any), we construct it and add it to the end of the heading so far. Then we add the "Category" heading to the heading line. Now the heading is finished, so we write it to cout. Finally, we call SelectItem to allow the user to select an item from our nicely formatted list, and return the result of that call to the calling function.

This function was the stimulus for a discussion of software development issues with Susan.

Susan: That sure is a lot of work just to handle item names of different lengths. Wouldn't it be simpler to assume a certain maximum size?
Steve: We'd still have to pad all the names before tacking the category on; the only simplification would be in the creation of the header, so it wouldn't really make the function much simpler.
Susan: What would happen if we had such a long name or category that the line wouldn't fit on the screen?
Steve: That's a very good question. In that case, the display would be messed up. However, I don't think that's very likely because the user probably wouldn't want to type in such a long name or category.
Susan: Okay. Now I have another question. If we were printing a report of all these items and categories, would each page line up differently from the others if it had a longer or shorter name length?
Steve: Well, so far we haven't implemented a report like that. However, if we did, each page would line up the same on any particular report because we go through the whole list to find the longest name. On the other hand, if we ran the report several times with different data, it is possible that the longest name would be of a different length in each report, so the columns wouldn't line up the same between reports.
Susan: So there really isn't any cut and dried way to make these decisions?
Steve: No, I'm afraid not. That's why they pay me the (relatively) big bucks as a software developer. I have to laugh whenever I see ads for "automatic bug-finder" software, especially when it claims to be able to find design flaws automatically. How does it know what problem I'm trying to solve?
The final function in this class is DeleteItem, whose code is shown in Figure 13.32.
FIGURE 13.32. The HomeInventory::DeleteItem function (from code\hmin8.cpp)
void HomeInventory::DeleteItem(short Index)
{
short ItemCount = m_Home.size();

for (short i = Index; i < ItemCount-1; i ++)
m_Home[i] = m_Home[i+1];

m_Home.resize(ItemCount-1);
}

This is another simple function. Starting at the item to be deleted, it moves all of the items after that point one position closer to the beginning of the inventory list and then reduces the size of the list by one. This effectively eliminates the selected item from the inventory.

13.19. Finishing up the HomeItem class

Now it's time to return to the HomeItem class. Luckily, the changes here are much smaller than the changes to the HomeInventory class. In fact, only one new function as been added to the HomeItem interface since the last version we looked at, hmit6.h. That function is GetCategory, whose base class version simply calls the derived class function of the same name, which merely returns the value of the m_Category variable in the item. We've seen enough of this type of function, so we won't bother going over it further.

However, some of the functions have changed in implementation, so we should take a look at them. We'll start with the only function declared in hmit6.h whose implementation has changed: operator >>, the code for which is shown in Figure 13.33.

FIGURE 13.33. The new operator >> implementation for the HomeItem classes (from code\hmit8.cpp)
istream& operator >> (istream& is, HomeItem& Item)
{
xstring Type;
bool Interactive = (&is == &cin);
HomeItem Temp;

Item = HomeItem();

while (Type == "")
{
if (Interactive)
{
cout << "Type (Basic(B), Music(M)) ";
cout << "or hit ENTER to exit: ";
HomeUtility::IgnoreTillCR();
getline(is,Type);
if (Type == "")
return is;
}
else
getline(is,Type);

if (is.fail() != 0)
return is;
}

if (toupper(Type[0]) == `B')
{
// set type of Temp to Basic object, to be filled in
Temp = HomeItem("",0.0,0,"","");
}
else if (toupper(Type[0]) == `M')
{
// set type of Temp to Music object, to be filled in
Temp = HomeItem("",0.0,0,"","","",Vec<xstring>(0));
}
else
{
xstring Message = "Bad object type: ";
Message = Message + Type;
HomeUtility::HandleError(Message);
return is;
}

Temp.Read(is);
Item = Temp;

if (is.fail() != 0)
HomeUtility::HandleError("Couldn't create object");

return is;
}

This function isn't too different from the last version we saw (Figure 11.32). The differences are as follows:
1. We are allowing the user to hit Enter to exit from this function without having to define a new item. This is useful when the user decides not to create a new item after selecting the "Add Item" function.
2. We are requiring only the first letter of the type rather than the whole type name. We are also allowing either upper- or lower-case type indicators. To do this, I've taken advantage of a standard library function left over from C called toupper, which simply returns an uppercase version of whatever character you call it with.
3. We are using the HandleError function to display the error message if the object type is invalid.
4. If the data for the object cannot be read from the input stream, we are displaying a message telling the user about that problem.
Now let's look at the changes in the HomeItemBasic implementation. We'll start with the Edit function, shown in Figure 13.34.
FIGURE 13.34. The latest version of HomeItemBasic::Edit (from code\hmit8.cpp)
void HomeItemBasic::Edit()
{
short FieldNumber;
bool result;

FormattedDisplay(cout);
cout << endl;

cout << "Please enter field number to be changed " <<
"or ENTER for none: ";

FieldNumber = HomeUtility::GetNumberOrEnter();

cout << endl;

if (FieldNumber == -1)
return;

EditField(FieldNumber);
}

This function differs from the previous version (Figure 11.39) only in its improved flexibility and error checking. Rather than simply asking the user to enter a field number and then assuming that the field number entered is valid, we use the GetNumberOrEnter function to allow the user to enter a field number or to just hit the Enter key to indicate that he or she has decided not to edit a field after all. Once we have received the return value from the GetNumberOrEnter function, we check to see whether it is the special value -1, which indicates that the user has decided not to enter a number but has just hit the Enter key. If this is the case, we simply return to the calling function without calling EditField to do the actual field modification. Otherwise, we call EditField to modify the selected field and return when it is finished.

The next function in the HomeItemBasic class we will cover is ReadInteractive, whose code is shown in Figure 13.35.

FIGURE 13.35. The latest version of HomeItemBasic::ReadInteractive (from code\hmit8.cpp)
short HomeItemBasic::ReadInteractive()
{
double PurchasePrice;
long PurchaseDate;
bool result;
xstring Dummy;

short FieldNumber = e_Name;

cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
FieldNumber ++;
getline(cin,m_Name);

cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << " (xxx.xx with no $ or ,): ";
FieldNumber ++;
result = HomeUtility::ReadDoubleFromLine(cin,PurchasePrice);
if (result)
m_PurchasePrice = PurchasePrice;
else
{
m_Name = "";
return 0;
}

cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << " (YYYYMMDD): ";
FieldNumber ++;
result = HomeUtility::ReadLongFromLine(cin,PurchaseDate);
if (result)
m_PurchaseDate = PurchaseDate;
else
{
m_Name = "";
return 0;
}

cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
FieldNumber ++;
getline(cin,m_Description);

cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
FieldNumber ++;
getline(cin,m_Category);

return FieldNumber;
}

The only difference between this version of the ReadInteractive function and the one in Figure 11.37 is its improved error checking and feedback to the user. In addition to making at least some attempt to check the validity of numbers and dates entered by the user, this new version also tells the user what sort of input is expected. In particular, it tells the user to type in the purchase price without using a $ or comma - it's entirely possible that the user might not realize that using these symbols would cause a problem in interpreting the value. This version also tells the user to type the date in the form YYYYMMDD rather than in a more familiar format such as MM/DD/YY. After telling the user how to enter these data items, it uses input functions that subject the numeric data entered to some minimal reasonableness checks. While far from airtight, this is a much safer approach than simply assuming that these values must be all right, as the previous version of the function did.

The changes to the next function we will cover, EditItem (Figure 13.36), are very similar to those in the previous function. To be precise, they consist of more error checking. These changes should be obvious enough that we don't have to discuss them.

FIGURE 13.36. The latest version of the HomeItemBasic::EditItem function (from code\hmit8.cpp)
bool HomeItemBasic::EditField(short FieldNumber)
{
bool result = true;
double PurchasePrice;
long PurchaseDate;
xstring Dummy;

switch (FieldNumber)
{
case e_Name:
cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
getline(cin,m_Name);
break;

case e_PurchasePrice:
cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
result = HomeUtility::ReadDoubleFromLine(cin,PurchasePrice);
if (result)
m_PurchasePrice = PurchasePrice;
break;

case e_PurchaseDate:
cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
result = HomeUtility::ReadLongFromLine(cin,PurchaseDate);
if (result)
m_PurchaseDate = PurchaseDate;
break;

case e_Description:
cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
getline(cin,m_Description);
break;

case e_Category:
cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
getline(cin,m_Category);
break;

default:
cout << endl;
HomeUtility::HandleError("Sorry, that is not a valid field number");
result = false;
break;
}

return result;
}

The two functions in the HomeItemMusic class, ReadInteractive and EditField, that have changed from the previous versions, follow the changes that we have just looked at very closely so I will list them without further comment.
FIGURE 13.37. The latest version of HomeItemMusic::ReadInteractive (from code\hmit8.cpp)
short HomeItemMusic::ReadInteractive()
{
long TrackCount;
bool result;
xstring Dummy;

short FieldNumber = HomeItemBasic::ReadInteractive();

// Check whether Basic input worked. If not, forget it.
if (FieldNumber == 0)
return 0;

cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
FieldNumber ++;
getline(cin,m_Artist);

cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << ": ";
FieldNumber ++;
result = HomeUtility::ReadLongFromLine(cin,TrackCount);
if (result)
m_Track.resize(TrackCount);
else
{
m_Name = "";
return 0;
}

for (short i = 0; i < TrackCount; i ++)
{
cout << FieldNumber << ". ";
cout << GetFieldName(FieldNumber) << i + 1 << ": ";
FieldNumber ++;
getline(cin,m_Track[i]);
}

return FieldNumber;
}

FIGURE 13.38. The latest version of HomeItemMusic::EditField (from code\hmit8.cpp)

bool HomeItemMusic::EditField(short FieldNumber)

{

if (FieldNumber < e_Artist)

{

return HomeItemBasic::EditField(FieldNumber);

}

bool result;

long TrackCount = m_Track.size();

switch (FieldNumber)

{

case e_Artist:

cout << FieldNumber << ". ";

cout << GetFieldName(FieldNumber) << ": ";

getline(cin,m_Artist);

return true;

case e_TrackCount:

cout << FieldNumber << ". ";

cout << GetFieldName(FieldNumber) << ": ";

result = HomeUtility::ReadLongFromLine(cin,TrackCount);

if (result)

m_Track.resize(TrackCount);

return result;

}

if (FieldNumber > (e_TrackCount + TrackCount))

{

HomeUtility::HandleError("Sorry, that is not a valid field number");

return false;

}

cout << FieldNumber << ". ";

cout << GetFieldName(FieldNumber);

cout << FieldNumber - e_TrackCount << ": ";

getline(cin,m_Track[FieldNumber - e_TrackNumber]);

return true;

}

13.20. Are We Having Fun Yet?

If nothing else, I hope that this analysis has given you a better appreciation of the difficulty of programming a solution to even an apparently simple problem in the real world. After reviewing what we've covered in the chapter, we'll get to some exercises that will give you an even better idea of how much fun programming can be!15

13.21. Review

We started this chapter with our work cut out for us; the program was performing as intended, so we just had to go over exactly how it worked. We started with the new main function, which consists of two consecutive "endless" loops (loops that execute until a particular criterion is met, rather than for a predetermined number of times). The first loop keeps executing as long as the user is still entering, modifying, or examining the data in the inventory. When the user is finished with these operations, the only remaining question is whether he or she wants to save the changes to the inventory, so the code in the second loop is designed to find the answer to that question and either save or discard the changes as desired.

The main work of the program is done inside the first loop, which consists of a call to the GetMenuChoice function to find out which operation the user wants to perform, followed by a call to the ExecuteMenuChoice function to execute that operation. When the user selects the "exit" operation, this loop terminates and allows the second loop to start execution.

The GetMenuChoice function is fairly simple, but it uses some functions we hadn't seen previously, including the (non-standard) clrscr function, which clears the screen, the GetCount function, which returns the number of items in the inventory, and the HandleError function from the HomeUtility namespace.

Once the GetMenuChoice function has determined which operation the user wants to perform, the ExecuteMenuChoice function takes over to execute it. It does this with a switch statement that contains one case for each possible operation. All of these cases are fairly similar. The main task of each of them is to request any information that might be required from the user, to display a heading telling the user what operation is in progress, and then to call a function in the Inventory class to do the actual work. If the operation results in a change to the database, the resulting inventory is saved in a backup file so that it can be recovered in the event of a power failure or other crash. Because of the similarity of the code in each of these cases, we won't review them further.

Instead, we will proceed to the functions of the HomeUtility namespace, starting with a set of functions that read numeric input either from the keyboard or a file. The first of these functions is called ReadDoubleFromLine, and as its name suggests, it reads a numeric value from a line of data into a double variable that is a reference argument. Then it calls CheckNumericInput to make sure that there are no garbage characters left in the line after reading the numerical value.

The next function in the HomeUtility namespace, ReadLongFromLine, is almost identical to ReadDoubleFromLine, with the obvious exception of the type of variable it fills in. So I won't bother discussing it further.

The next function, ReadDateFromLine, however, is a little bit more complicated, as it attempts to do a little bit of validation on the data it is reading in from the user or file, and displays an error message if it doesn't like the data being entered.

Next, we have a particularly simple function, IgnoreTillCR. This function ignores characters from an input stream until it gets to a "carriage return"16, which is generated when the Enter key is struck. The next function is HandleError, which is used to display an error message and wait for the user to hit Enter.

Now we get to GetNumberOrEnter, which is considerably more complicated than the other functions in the HomeUtility namespace, as it has the more complex task of taking input from the user one keystroke at a time. This function actually has two "modes" of operation. In the first mode, it accepts only digits, Enter, and the backspace key, which is used for correcting errors; in the second mode, it also accepts the up and down arrow keys - this mode is used when the user wants to select an item from a list via the SelectItem function. While going through this function, we ran into several new constructs, the most significant being the (non-standard) windos::getkey function that allows us to read one key from the keyboard without having to wait for the user to hit the Enter key, as is necessary when we use the standard C++ stream input functions. In addition to getkey, we also discussed a set of special keys, such as backspace and newline, which have to be handled differently from the "normal" digit keys in this function. We also saw that it is necessary to copy the key value from an int to a char variable before displaying it on the screen if we want it to come out in the proper format. Sending an int to cout via operator << will display the numeric value of the int, which in this case would not be informative to the user who is expecting to see the key he or she just pressed!

After we covered the details of this GetNumberOrEnter function, including the way in which it handles the backspace key so that the user can back up and change the value of the number, we moved on to the relatively simple HomeUtility::ClearRestOfScreen function, which is used by SelectItem to erase the part of the screen it uses to display its list of items. Even though this ClearRestOfScreen function isn't very complicated, it deserved some discussion because it was the first one where we used a couple of the screen-handling functions from the (non-standard) conio (console I/O) library: gotoxy and clreol, along with another non-standard function called windos::ScreenRows. The gotoxy function, as its name suggests, allows us to position the "cursor" (the place where the next character will be written on the screen) to a particular X and Y coordinate: X is the column number and Y is the row number. Unusually for C or C++, this function starts counting at 1; that is, the first row and first column are numbered 1 rather than 0. The clreol function erases some or all of the characters on the line where the cursor is currently located, from the cursor's position rightward to the end of the line. The windos::ScreenRows function tells us the number of lines on the screen that we can use, which we need so we will know where to stop clearing lines.

HomeUtility::SelectItem, the next function we discussed, is responsible for allowing the user to select from a list of items. It has two arguments: Number, a Vec of indexes into the inventory list of the items to be displayed, and Name, a Vec of textual information about each of those items. It uses these arguments to display a formatted list of items, handling a large number of items by scrolling a portion of the list onto the screen at any one time. Once the user selects one of the items, it returns the information about which one was selected to the calling function.

The final function in this namespace is CheckNumericInput, which is called after every numeric input operation. It determines whether the previous input operation was successful by looking to see whether the next character waiting in the input stream is a newline. If so, all of the characters up to that point have been accepted by the input operator as part of a numeric value, which means that the user didn't type anything that shouldn't be in a number. In that case, the function returns the value true to its caller to indicate success. However, if the next character in the input stream isn't a newline, the user must have included some inappropriate character(s) in the input. In that case, this function displays the leftover characters and returns the value false to the calling function to inform it of the error.

After dealing with that final function in the HomeUtility namespace, we moved on to the changes in the HomeInventory class. These changes weren't too extensive, primarily consisting of better error checking and sorting of the inventory list by the name of the item. Other changes included the ability of the "locate" functions to return a Vec of item indexes rather than just one, the addition of functions to locate items by their category fields, and the ability to print items or item names. The most complicated function in this new version of the class, besides the sorting function, is SelectItemFromCategoryList, because it uses some fairly fancy formatting to get the category information to line up correctly. The main complexity is caused by the necessity to pad the item names to a consistent length so that the category information will start in the same column on the screen for each item no matter how long its name may be. After we have created and displayed the heading, we call the SelectItem function to allow the user to select one of the items.

The final function in the HomeInventory class that we discussed is DeleteItem, which deletes an item from the inventory list.

The changes to the HomeItem class were relatively small. We modified the implementation of operator >> to allow the user to hit Enter to avoid entering an item, to allow the user to enter only the first letter of the type rather than having to type "Basic" or "Music", and to improve error handling. The changes in the HomeItemBasic and HomeItemMusic classes consisted of simple improvements to error handling and flexibility in user input, so they didn't require any additional discussion.

13.22. Exercises

1. Draw a picture of the operation of the find_nocase function when it is searching for the value "purple" in an xstring with the value "A Purple couch".
2. Add the ability to edit an existing description field rather than having to type the whole field in again. Which classes will have to be changed to do this?
3. Before writing over the old version of the data base at the conclusion of the program, copy that file to another name so that the user can go back to it if necessary.
4. At present, the ExecuteMenuChoice function always saves the inventory in the backup file after calling AddItem. This is not necessary if the user didn't actually add a new item. Fix the code so that this unnecessary operation is avoided.
5. Add the ability for the user to select a category from a list of those that already exist. Of course, you still have to allow the user to specify a new category when desired.
6. Add the ability for the user to see only items of a given type, such as "Basic" or "Music", rather than having to see all types together in one list.
7. Change the data entry function so that the user can correct an entry and continue entering data for an item even after making an error.
8. Add validation of dates so that invalid dates such as 19970231 are not accepted.
9. Allow the user to type dates with or without slashes so that 1997/02/28 will be the same as 19970228.
10. Add the ability for the user to print selected items according to category, description, and the like.
11. Explain why hitting the Enter key when asked for a category produces a list including all categories.
12. Implement the HomeItemCollectible class as a derived class of HomeItemBasic to keep track of objects such as coins or stamps. The added fields should include date of creation, condition, artist, and any other appropriate fields.

13.23. Conclusion

If you've made it all the way through this book, congratulations! You have truly begun to understand the complexities of C++ in particular and software development in general. At this point, you should be able to read almost any intermediate-level book on software development in C++ with profit and understanding.
1 One possible pronunciation of this construct is "forever".

2 The clrscr function is one of a few functions that I've had to write to allow our programs to use underlying operating system I/O functions, because the facilities they provide don't exist in standard C++. You'll have to replace them if you want to run with another compiler or operating system. A list of these functions is in the "readme.txt" file on the CD.

3 We'll get to the purpose and implementation of the HomeUtility namespace functions in the section "Using a namespace to Group Utility Functions" on page 925.

4 Please note that the C++ standard does not specify the name of the stream to which the system printer is connected. However, lpt1 is a typical name for the stream that is connected to the system printer in a Windows environment.

5 Actually, this isn't quite correct. As I mentioned earlier, since every scope (and therefore every class) is a namespace, we really have been creating namespaces all along without even trying. However, this is the first namespace we've created that has a name and isn't a class.

6 By an interesting coincidence, after first writing this paragraph I had exactly this problem when trying to reuse some old code I had written. This old code defined global names that conflicted with another library that the user of this code also needed. I fixed this by changing the names, but having kept them out of the global namespace would have prevented the problem.

7 Actually, as I have pointed out previously, all of the data types in C++ can vary in size from one compiler to another. However, int is more likely to be different in size from one compiler to another than, for example, short.

8 Note that windos::getkey is not a standard library function.

9 This buffering technique is also applied to input. Rather than read one character at a time from an input file, the operating system reads a number of characters at a time and gives them to the program when it asks for them. This greatly improves the efficiency of reading from a file; however, it is much less useful when reading data from the keyboard, as the user doesn't know what to type before we provide a prompt.

10 Please note that the conio library is not part of standard C++. However, it is impossible to do anything other than the most primitive form of screen output using standard C++ exclusively. Therefore, we have no choice but to use a non-standard library in this case.

11 As implemented in the code on the CD, this function returns the value "25", which obviously is less than the number of screen rows you're likely to have. I left it that way because it helps in testing the program, as you'll see later.

12 If SelectItem returns the value 0, then SelectItemByPartialName will return -1, which will tell the calling function that nothing was selected.

13 I should mention here that it is not a good idea to use "magic" numbers in programs. These are numbers that do not have any obvious relationship to the rest of the code. A good general rule is that numbers other than 0, 1, or other self-evident values should be defined as const or enum values rather than as literal values like `7'. That's why I've defined a const value for the item number length, even though that value is used in only one place.

14 By the way, neglecting the possibility that the heading is already long enough caused a "virtual memory exceeded" error message in an earlier version of this program. The problem was that the length of the padding was calculated as a negative value. However, the argument to operator new is unsigned. Therefore, when I asked it to allocate (for example) -3 characters of memory, it tried to give me approximately 4 billion bytes. Fortunately, that exceeds the maximum amount my operating system can handle, so I got an error message.

15 I'm sure you're just brimming with excitement at that thought, but please try to hold yourself back until you have read the review, so you don't lose track of what we've already covered in this chapter.

16 This is another name for the character known as "newline".


TOC PREV NEXT INDEX