Mad Libs Again

Background

For this exercise we will write a more complete madlibs program. This is not a final solution, that needs to wait for arrays next semester, but it is a piece wise refinement. To complete this exercise, you need to follow the instructions and make a working madlib program.

The program should begin by asking the user for an input file. This file is structured as follows:

An example of the input file is here
The Name of a Person
A Type of a business
An Animal
A Sound

Old <1>

Old <1> had a <2>.
E-I-E-I-O
And on his <2> he had a <3>,
E-I-E-I-O
With a <4>-<4> here and a <4>-<4> there
Here a <4> there a <4>,
Everywhere a <4>-<4>.
Old <1> had a <2>.
E-I-E-I-O

Your program should continue to prompt the user for a file name until a file has been opened.

An example dialog might be:

[bennett@mirkwood extra]$ solution
Enter the name of a madlib file => bob

Attempting to open bob
Unable to open bob


Enter the name of a madlib file => one.mad

Attempting to open one.mad
Success 

Once the file is successfully opened, the user should be presented with each of the descriptions and allowed to provide a phrase that matches that description. The phrase should be approved by the user before the program continues. An example of this interaction might be:

Enter The Name of a Person => bob
You said that bob was The Name of a Person
Is this ok? (Y,N) => n
Enter The Name of a Person => MacDonald
You said that MacDonald was The Name of a Person
Is this ok? (Y,N) => Y

Once all four phrases have been input, your program should then proceed to tell the story in the madlib. This is done by finding all instances of <n> and replacing them with the corresponding phrase. For example the line

Old <1>
should become
Old MacDonald
A complete run of the program:
Enter the name of a madlib file => one.mad
Attempting to open one.mad
Success 


Enter The Name of a Person => MacDonald
You said that MacDonald was The Name of a Person
Is this ok? (Y,N) => y
Enter A Type of a business => farm
You said that farm was A Type of a business
Is this ok? (Y,N) => y
Enter An Animal => pig
You said that pig was An Animal
Is this ok? (Y,N) => y
Enter A Sound => oink
You said that oink was A Sound
Is this ok? (Y,N) => y

We will now tell a story about MacDonald, farm, pig and oink


Old MacDonald

Old MacDonald had a farm.
E-I-E-I-O
And on his farm he had a pig,
E-I-E-I-O
With a oink-oink here and a oink-oink there
Here a oink there a oink,
Everywhere a oink-oink.
Old MacDonald had a farm.
E-I-E-I-O

The Algorithm

The first step is to design an algorithm. I present the following
Main Algorithm:

Open the file

Get a Blank Filler for item 1
Get a Blank Filler for item 2
Get a Blank Filler for item 3
Get a Blank Filler for item 4

get a line
while not end of file
    process a line
    get a line
close the files.

Open The file:
   Input: none
   Output:  an open file stream
   while no open file
      Get and validate the file name
      Open the file

Get a Blank Filler:
   Input: the file stream
   Output: a string to fill the blank with.

   read in the line
   ask the user for input
   while the user is not happy with result
       ask the user for input

Process A line:
   Input The line and four blank fillers
   
   location = find next angle
   while location != npos
      get text to <
      get tag
      line = text past >
      if tag is an a
         get next tag

From this design, I see at least three functions

There are possibly more, but these will do for a start.

The Program

Begin with the basic C program shell
#include <iostream>
#include <fstream>
#include <string>

using namespace std;

int main() {


    return 0;
}

When declaring a function, you first need to declare a function prototype. This is based on what the function will do. Right now we are not sure, so we will just create function prototype shells.

Between using namespace std and int main() place

using namespace std;

void OpenFile(void);
void GetBlank(void);
void ProcessLine(void);

int main() {
The purpose of these prototypes is to tell the program how to use the functions. We will modify these later. This is the equivalent to declaring a variable.

Next we should define the functions. This occurs after the end of main. Again, we will just put place holders in for now. Add the following after main:

    return 0;
}

void OpenFile(void){
   cout << "In Open Files" << endl;
   return;
}

void GetBlank(void){
   cout << "In Get Blank " << endl;
   return;
}

void ProcessLine(void){
   cout << "In Process Line" << endl;
   return;
}

We should put something in main to start to check our logic. The first few lines of the algorithm are fine, but the while loop is problematic for now. Change main to be

int main() {
    // open the file
    OpenFile();

    // read the four blank values from the file
    GetBlank();
    GetBlank();
    GetBlank();
    GetBlank();

    // tell the story
    // get a line
    // while (not end of file) {
             ProcessLine();
    //       get a line
    // }

    return 0;
}

This file is here and can be compiled and run.

Running this program produces

In Open Files
In Get Blank 
In Get Blank 
In Get Blank 
In Get Blank 
In Process Line
Perhaps not the best, but it does show that the functions are being called.

There is little that can be done in the way of testing the program until the file is open, so working on OpenFiles seems a next logical step.

We need to think about what data the function needs from the program and what data it will return. In this case, it needs no data, but will return an open file stream. We will do this by

Writing the OpenFile function.

The algorithm is fairly straight forward

done = false
while not done 
    get the name of the file from user
    try to open the file
      if successful
         done = true;

The code is reasonably straight forward as well. We do want some local variables, but it turns out that we can just declare them as normal.

void OpenFile(ifstream & file){
   string fileName;
   bool fileOpen = false;

   while (not fileOpen) {
       cout << "Enter the name of a madlib file => ";
       cin >> fileName;
       cin.ignore(100,'\n');

       cout << endl;
       cout << "Attempting to open " << fileName << endl;

       file.open(fileName.c_str());

       if (file) {
           cout << "Success " << endl;
           fileOpen = true;
       } else {
           cout << "Unable to open " << fileName << endl;
       }
       cout << endl << endl;
   }

   return;

Finally, once we have opend the file, we should close it, so in main add a close

    inFile.close();
    return 0;
}

This completes a function definition.

Running step2 would result in
[bennett@mirkwood extra]$ step2
Enter the name of a madlib file => bob

Attempting to open bob
Unable to open bob


Enter the name of a madlib file => one.mad 

Attempting to open one.mad
Success 


In Get Blank 
In Get Blank 
In Get Blank 
In Get Blank 
In Process Line

Again not superior, but we now have the file open, so we can change the main routine to actually read the file

int main() {
    ifstream inFile;
    string line;
    
    // open the file
    OpenFile(inFile);
    
    // read the four blank values from the file
    GetBlank();
    GetBlank();
    GetBlank();
    GetBlank();
    
    // tell the story
    getline(inFile, line);
    while (inFile) {
        cout << line << endl;
        ProcessLine();
        getline(inFile, line);
    }

    return 0;
}

The code is here. If you run this code, you will see that it prints out the entire file.

The GetBlank Function

The algorithm for get blank is fairly simple
read a line from the file
done = false
while not done
   ask the user for a value
   print the value
   ask the user if the value is right
   if yes 
      done = true

This function needs to pass two pieces of information

Thus the function will have two parameters.

The function prototype (and function header) should then become

void GetBlank(string & result, ifstream & file);
...
void GetBlank(string & result, ifstream & file){

We will need space to store each of the responses so in the main routine

int main() {
    ifstream  inFile;
    string blank1, blank2, blank3, blank4;
    string line;

    OpenFile(inFile);
    GetBlank(blank1, inFile);
    GetBlank(blank2, inFile);
    GetBlank(blank3, inFile);
    GetBlank(blank4, inFile);

Finally the code for GetBlank

void GetBlank(string & result, ifstream & file){
   string description;
   char answer;
   bool goodAnswer = false;

   getline(file, description);
   if (file) {
       while (not goodAnswer) {
           cout << "Enter " << description << " => ";
           getline(cin, result);
           cout << "You said that " << result << " was " << description << endl;
           cout << "Is this ok? (Y,N) => ";
           cin >> answer;
           cin.ignore(100,'\n');
           answer = toupper(answer);
           if (answer == 'Y') {
              goodAnswer = true;
           }
       }
       cout << endl;
   }

   return;

The code so far is contained in this file.

It might be nice to tell the user that we are about to tell them a story. I added another function, Introduction to do just that. I will pass it the four blank values, and print out an appropriate message.

Since we will not change the four blank values in the Introduction function, they will be passed by value or they will not have an & in the parameter list.

The function prototype, and function definition are

void Introduction(string lineA, string lineB, string lineC, string lineD);
...
void Introduction(string lineA, string lineB, string lineC, string lineD) {
    cout << endl;
    cout << "We will now tell a story about " << lineA << ", " << lineB
         << ", " <<  lineC << " and " << lineD << endl;
    cout << endl;

    return;
}

And in the main function

    GetBlank(blank3, inFile);
    GetBlank(blank4, inFile);

    if (inFile) {
        Introduction(blank1, blank2, blank3, blank4);
    }

The entire code to this point is here.

ProcessLine

The input to ProcessLine is the line, and the four blank variables. Since none of these will change in the function, they are all passed by value. The function prototype and function headers are as follows
void ProcessLine(string line, string a, string b, string c, string d);
...
void ProcessLine(string line, string a, string b, string c, string d){

For me, this function is way too difficult to write as a single piece, so I am going to design with functions in mind. The algorithm is

while there is a tag in the line
   outputLine = outputLine + text before tag + text from tag
   line = text after tag 
outputLine = outputLine + line
print outputLine

The code eventually becomes

void ProcessLine(string line, string a, string b, string c, string d){
   size_t startPos, endPos;
   string outputLine;
   string tag;

   while (GetTag(line, startPos, endPos, tag)) {
       outputLine = outputLine + line.substr(0, startPos);
       line = line.substr(endPos+1, line.size());
       outputLine = outputLine + FillBlank(tag, a,b,c,d);
   }

   outputLine += line;

   cout << outputLine << endl;
   return;
}

Notice this uses two functions, GetTag and FillBlank.

FillBlank is the easiest of these functions to write.

This function is different because it returns a value, or is a value returning function.

The function is:

string FillBlank(string tag, string a, string b, string c, string d) {
   string giveBack;
   if(tag == "1") {
      giveBack = a;
   } else if (tag =="2") {
      giveBack = b;
   } else if (tag == "3") {
      giveBack = c;
   } else if (tag == "4") {
      giveBack = d;
   } else {
      giveBack = "Bad Tag";
   }

   return giveBack;
}

Finally the function GetTag It takes the line, and will return the point where the tag starts, and ends, as well as the text of the tag. In addition it will return true if a tag is found and false if a tag is not found.

bool GetTag(string line, size_t & startPos, size_t & endPos, string & tag) {
     size_t pos;
     bool returnValue = true;

     startPos = line.find("<") ;
     if (startPos == string::npos) {
        returnValue = false;
     }
     endPos = line.find(">", startPos);
     tag = line.substr(startPos+1, endPos-startPos-1);

     return returnValue;
}
The final code is here.