hanipman
hanipman

Reputation: 79

I need an alternate method to streaming strings. C++

So my project is to take create a program that takes an input that looks similar to this:

Boole, George       98  105 -1  -1  -1
Pascal, Blaise      63  48  92  92  92
Babbage, Charles    100 97  100 98  -1
Kepler, Johannes    75  102 100 -1  -1
Clown, Bozo         0   6   6   57  62
Fini, End          -99  -99 -99 -99 -99

And output this:

Student         Submission     Grade
Boole, George       2           105
Pascal, Blaise      3           92
Babbage, Charles    1           100
Kepler, Johannes    2           102
Clown, Bozo         5           62

I'm having trouble because my current code can successfully compile it, but one of my other input files follows a different format. My current code:

int main() 
{

    ifstream infile;
    ofstream outfile;
    infile.open("./ProgGrades1.txt");
    outfile.open("./GradeReporttest.txt");

    string lastName, firstName;

    int score1, score2, score3, score4, score5;
    int max, location;

    while(GetInput(infile, lastName, firstName, score1, score2, score3, score4,
            score5))
    {
        if (score1 == -99)
            break;
        AnalyzeGrade(infile, lastName, firstName, score1, score2, score3, 
               score4, score5, max, location);

        WriteOutput(infile, outfile, lastName, firstName, max, location);

        cout << lastName << " " << firstName << " " << location << " " << max <<
                endl;
    }

    infile.close();
    outfile.close();
    return 0;
}

int GetInput(ifstream& infile, string& lastName, string& firstName, int& score1,
        int& score2, int& score3, int& score4, int& score5)
{
    infile >> lastName >> firstName >> score1 >> score2 >> score3 >> 
            score4 >> score5;
    return infile;
}


int AnalyzeGrade(ifstream& infile, string& lastName, string& firstName, 
        int& score1, int& score2, int& score3, int& score4, int& score5, 
        int& max, int& location)
{
    int score[5];
    max = 0;
    score[0] = score1;
    score[1] = score2;
    score[2] = score3;
    score[3] = score4;
    score[4] = score5;

    for (int i = 0; i < 5; i++)
    {
        if (score[i] > max)
        {
            max = score[i];
        }
    }

    if (max == score[0])
    {
        location = 1;
    }
    else if (max == score[1])
    {
        location = 2;
    }
    else if (max == score[2])
    {
        location = 3;
    }
    else if (max == score[3])
    {
        location = 4;
    }
    else if (max == score[4])
    {
        location = 5;
    }
    else
    {

    }

    fill_n(score, 6, 0);
    return infile;
}

void WriteOutput(ifstream& infile, ofstream& outfile, string& lastName, 
        string& firstName, int& max, int& location)
{
    string studentID = lastName + " " + firstName;
    outfile << "\n" << setw(19) << studentID << setw(14) << location << " " << 
            max;
}

My other input file looks like:

Stroustrup, Bjarne  8   8   -1  -1  -1
Lovelace, Ada       1   60  14  43  -1
von Neumann, Jon    77  48  65  -1  -1
Wirth, Niklaus      51  59  -1  -1  -1
Wozniak, Steve      81  -1  -1  -1  -1
Babbage, Charles    31  92  -1  -1  -1
Hopper, Grace       76  -1  -1  -1  -1
Bird, Tweety        -99 -99 -99 -99 -99
Sylvester           77  39  -1  -1  -1

So the problem here is that my infile streams in two strings, but on line 3 there are two parts to the last name, and for the last line, there is one name. I need an alternate method to obtain the names.

Btw I'm currently in an intro to C++ course, so my knowledge is limited, but I have no qualms researching. As you can see, I'm using more entry level code. I tried to use arrays, but I concluded that I still don't understand how to pass them successfully.

Upvotes: 0

Views: 149

Answers (4)

Johan
Johan

Reputation: 3778

Just for fun, a program using boost::spirit that does the job. There is surely a cleaner way to handle strings.

#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/home/phoenix/object/construct.hpp>
#include <boost/spirit/home/phoenix/container.hpp>
#include <boost/spirit/include/phoenix_core.hpp>
#include <boost/optional.hpp>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>

struct Student
{
    boost::optional<std::string> first_name;
    std::string last_name;
    std::vector<signed long> grades;

    bool fill(const std::string& str)
    {
        namespace qi = boost::spirit::qi;
        namespace ascii = boost::spirit::ascii;
        namespace phoenix = boost::phoenix;

        typedef std::vector<char> chars;
        auto set_last_name = 
            [this](const std::vector<char>& name)
            {
                last_name = std::string(name.begin(), name.end());
            };
        auto set_first_name = 
            [this](const std::vector<char>& name)
            {
                first_name = std::string(name.begin(), name.end());
            };

        bool r = qi::phrase_parse(str.begin(), str.end(),
            (
                (+qi::alpha)[ set_last_name ] // gives vector of char
                >> -(',' >> +qi::alpha)[ set_first_name ] // gives vector of char
                >> *(qi::int_ [ phoenix::push_back(phoenix::ref(grades), qi::_1) ])
            ), qi::space);    

        return r;
    }
};


int main(int argc, char* argv[])
{    
    if (argc < 2)
    {
        std::cout << "Please specify a filename" << std::endl;
        return -1;
    }

    std::ifstream file(argv[1]);

    if (!file)
    {
         std::cout << "Invalid filename: " << argv[1] << std::endl;   
         return -2;
    }

    std::vector<Student> students;
    std::string str;
    while (getline(file, str))
    {       
        Student student;
        if (student.fill(str))
        {
            std::cout << "Parsing succeeded, adding '" ;

            if (student.first_name)
            {
                 std::cout << *student.first_name << " ";
            }

            std::cout 
                << student.last_name 
                << "' with " << student.grades.size() << " grades." 
                << std::endl;
            students.push_back(student);
        }
        else
        {
            std::cout << "Parsing failed." << std::endl;
        }
    }

    return 0;
}

And here is the output:

$ ./a.exe input.txt
Parsing succeeded, adding 'Bjarne Stroustrup' with 5 grades.
Parsing succeeded, adding 'Ada Lovelace' with 5 grades.
Parsing succeeded, adding 'Jon vonNeumann' with 5 grades.
Parsing succeeded, adding 'Niklaus Wirth' with 5 grades.
Parsing succeeded, adding 'Steve Wozniak' with 5 grades.
Parsing succeeded, adding 'Charles Babbage' with 5 grades.
Parsing succeeded, adding 'Grace Hopper' with 5 grades.
Parsing succeeded, adding 'Tweety Bird' with 5 grades.
Parsing succeeded, adding 'Sylvester' with 5 grades.

Upvotes: 0

Eugene
Eugene

Reputation: 71

You should tokenize your input string and implement slightly more complecated parsing. You may use boost::split in your GetInput function or just strtok function. Like this:

int GetInput(ifstream& infile, string& lastName, string& firstName, int& score1,
        int& score2, int& score3, int& score4, int& score5)
{
    std::string line = infile.str ();
    std::list<std::string> tokens; // or something fancy with boost::iterator_range

    boost::split (tokens, line, boost::is_any_of(",")); // define your own predicate if needed
    // check result and tokens collection before processing it
    std::list<std::string>::iterator it = tokens.begin();
    lastName.swap(*it++);
    // now you should split rightmost token the same way but with space between tokens...

    return infile;
}

But the proper solution would be trying regular expressions. In C++11 world you may use regex package.

Upvotes: 1

user1508519
user1508519

Reputation:

This is very verbose, but demonstrates the use of iterators. I use a stringstream for demonstrative purposes. Remove , John to see how it handles no first name.

Note, I grab trim from here, but don't post the code here for brevity.

#include <iostream>
#include <string>
#include <sstream>
#include <algorithm>
#include <cctype>

int main() {
  std::string line = "von Neumann, John    77  48  65  -1  -1";
  std::istringstream iss(line);
  auto it = std::find(line.begin(), line.end(), ',');
  std::string last_name;
  std::string first_name;
  std::string integer_list;

  // If we didn't find a comma
  if (it == line.end())
  {
      // We have a last name only
      first_name = "NO_FIRST_NAME";
      // No comma, so we just search up to the first integer
      auto find_integer_it = std::find_if(line.begin(), line.end(), [] (char c) { return isdigit(c); });
      last_name = std::string(line.begin(), find_integer_it);
      // Get rest of string from the position of first integer
      integer_list = std::string(find_integer_it, line.end());
      trim(last_name);
  } else {
    last_name = std::string(line.begin(), it);

    // it+2 because we're skipping the comma
    // and the whitespace after the comma
    auto space_it = std::find(it+2, line.end(), ' ');
    first_name = std::string(it+2, space_it);
    auto find_integer_it = std::find_if(line.begin(), line.end(), [] (char c) { return isdigit(c); });
    integer_list = std::string(find_integer_it, line.end());
  }
  std::cout << last_name << ", " << first_name << std::endl;
  std::cout << integer_list << std::endl;
}

Output:

von Neumann, John

77  48  65  -1  -1

At this point, it should be trivial to parse integer_list.

Upvotes: 0

wl2776
wl2776

Reputation: 4327

You need better format specification. Both of your files look like fixed width-formatted files.

Names with spaces occupy first 19 characters, grades start from 20th position, each grade occupies 3 characters.

You can play on that.

Upvotes: 1

Related Questions