Parsing a .txt file using operator overloading

127 views Asked by At

This is for a school assignment. I am overloading >> so that I can parse a text file and assign it to a class.

For some reason, I can only read one object from the .txt file.

Here is my attempt at operator overloading:

std::istream &operator>>(std::istream &is, ExercisePlan &plan) {
    std::string name;
    int steps;
    std::string date;

    std::getline(is, name);
    is >> steps;
    is.ignore();
    std::getline(is >> std::ws, date);

    plan.setPlanName(name);
    plan.setGoalSteps(steps);
    plan.setDate(date);

    return is;
}

Here is where I call my operator overloader:

template<typename T>
void FitnessAppWrapper::loadDailyPlan(std::fstream &fileStream, T& plan) {
    fileStream >> plan;
}
void FitnessAppWrapper::loadWeeklyPlan(std::fstream &fileStream, DietPlan *weeklyPlan) {
    for(int i = 0; i < 7; i++){
        //loadDailyPlan(fileStream,weeklyPlan[i]);
        fileStream >> weeklyPlan[i];
    }
}

Here is my text file, ExercisePlans.txt

Morning Walk
5000
02/22/2024

Gym Session
6000
02/23/2024

Cycling
5500
02/24/2024

Swimming
7000
02/25/2024

Running
8000
02/26/2024

Yoga
4500
02/27/2024

Hiking
7000
02/28/2024

Here is what the parser reads (I have a display function that displays the information):

|Exercise Plan: Morning Walk
|Date: 02/22/2024
|Goal: 5000 Steps

|Exercise Plan: 
|Date: 
|Goal: 0 Steps

|Exercise Plan: 
|Date: 
|Goal: 0 Steps

|Exercise Plan: 
|Date: 
|Goal: 0 Steps

|Exercise Plan: 
|Date: 
|Goal: 0 Steps

|Exercise Plan: 
|Date: 
|Goal: 0 Steps

|Exercise Plan: 
|Date: 
|Goal: 0 Steps

I've went through the code in debugger, and I found that when I try to read the second exercise plan, it just stops reading the file?

If needed, here is my display function:

std::ostream &operator<<(std::ostream &os, const ExercisePlan &plan) {
    os << "|Exercise Plan: " << plan.getPlanName() << "\n";
    os << "|Date: " << plan.getDate() << "\n";
    os << "|Goal: " << plan.getGoalSteps() << " Steps\n";

    /*os << plan.getPlanName() << "\n";
    os << plan.getDate() << "\n";
    os << plan.getGoalSteps() << "\n";*/
    return os;
}

If needed, here is my class:

class ExercisePlan {
private:
    int goalSteps;
    std::string planName;
    std::string date;
public:
    // ## SETTERS AND GETTERS
    int getGoalSteps() const;
    void setGoalSteps(int goalSteps);
    const string &getPlanName() const;
    void setPlanName(const string &planName);
    const string &getDate() const;
    void setDate(const string &date);

    // ## CONSTRUCTORS
    ExercisePlan();
    ExercisePlan(int steps, const std::string& name, const std::string& date);
    ExercisePlan(const ExercisePlan& other);

    // ## DESTRUCTOR
    ~ExercisePlan();

    // ## OTHER FUNCTIONS
    void editGoal();
    friend std::ostream& operator<<(std::ostream& os, const ExercisePlan& plan);
    friend std::istream& operator>>(std::istream& is, ExercisePlan& plan);
    friend std::fstream& operator<<(std::fstream &fileStream, ExercisePlan &plan);
};

The reason why I am using function overloading and operator overloading is because I have a similar class to ExercisePlan, called DietPlan, with a similar .txt file that I also need to parse.

3

There are 3 answers

0
John Park On

This is a simplified code from your question. I guess you intended to skip white spaces between each data, but you're calling ifs.ignore() before extracting one is completed.

#include <fstream>
#include <iostream>
#include <string>

int main() {
  std::ifstream ifs("ExercisePlans.txt");

  for (int i = 0; i < 7; ++i) {
    std::string name;
    int steps;
    std::string date;

    std::getline(ifs, name);
    ifs >> steps;
    ifs.ignore();  // WRONG. should be called after the following line
    std::getline(ifs >> std::ws, date);

    std::cout << "status of ifs: " << (ifs.good() ? "GOOD" : "ERR") << "\n";
    std::cout << name << " " << steps << " " << date << "\n\n";
  }

  return 0;
}

I tested switching std::getline(ifs>>std::ws,date) and ifs.ignore() worked.

In addition, you can check if your stream operations are successful by calling ifs.good(), ifs.fail() or ifs.bad(). See how the status of stream changed with the above code I put.

0
Chris On

There's an empty line between each set of information in your file.

std::getline(is, name);

This reads that empty line, then you try to read the name as an int and the stream goes into a failure state.

You need to account for this empty line between each extraction from the input stream.

0
tbxfreeware On

@Chris identified one of the biggest problems: you fail to read the blank line between records. After the first record, for instance, the blank line is being read into the name field of the second record.

You should have an extra call to std::getline that reads the blank line into a dummy variable. Otherwise, you could call is.ignore(std::numeric_limits<std::streamsize>::max(), '\n') to skip over it. Either way, you have to be careful, because there is no blank line following the final record in the file. If you try to read one there, you will fail.

I prefer to read the blank line into a dummy variable. That way, I can check to see whether the line is truly blank.

main.cpp

Reading a file is not difficult. It is, however, tedious and cumbersome, and therefore, error-prone. You have to check every single input operation to see whether it failed.

My plan to is to throw an exception if anything goes awry. That is why I coded function main for this example as a function-try-block. You could just as easily use a regular try-block.

int main()
try {
    std::string e_fileName{ "ExercisePlans.txt" };
    std::vector<ExercisePlan> e_plans;
    load_exercise_plans(e_fileName, e_plans);  // Read the file into a vector.
    display_exercise_plans(std::cout, e_plans);
    return 0;
}
catch (std::exception const& e) {
    std::cerr << e.what() << "\n\n";
    return 1;
}

load_exercise_plans

Function load_exercise_plans goes through the usual rigmarole of checking whether the file could be opened. After that, it runs a loop that reads one ExercisePlan per iteration, and pushes it onto the back of vector e_plans.

Records are read using operator>>.

Most of the errors detected by operator>> cause an exception to be thrown. If std::getline fails at the start of a record, however, due to an end-of-file condition, the error is ignored. The input stream will still be placed into a failed state, but no exceptions are thrown. This allows the loop in function load_exercise_plans to end "normally," when operator>> fails to read a record at the end of the file.

The file is closed automatically at the end of function load_exercise_plans, when the destructor of variable ifs is called (or when the stack unwinds after an exception is thrown).

void load_exercise_plans(
    std::string const& file_name, 
    std::vector<ExercisePlan>& e_plans)
{
    std::ifstream ifs{ file_name };
    if (!ifs.is_open())
        throw std::runtime_error(
            "could not open file: " + file_name);
    ExercisePlan p;
    while (ifs >> p)
        e_plans.push_back(p);
}

display_exercise_plans

Function display_exercise_plans runs a simple loop to display the records in vector e_plans. Records are output using operator<<.

void display_exercise_plans(
    std::ostream& ost,
    std::vector<ExercisePlan> const& e_plans)
{
    for (auto const& p : e_plans)
        ost << p;
}

I made only one change to operator<<, which was to output a blank line at the end of each record. Otherwise, I would not have been able to duplicate the output shown in the OP.

As the modification to operator<< was trivial, I will not show its source code.

Parse integer fields with a stringstream

The version of operator>> given below is somewhat pedantic, but it shows the detailed checking that is required in order to provide precise error diagnostics.

The code that inputs the integer variable steps is interesting. It employs an idiom I use frequently.

  • Step 1. Call std::getline to read the line containing steps into a string. This enforces the requirement that the value for steps be stored by itself on a line of the file.
  • Step 2. Parse the string by loading it into an istringstream.

After inputing steps from the istringstream, the I/O manipulator std::ws is used to read and discard trailing whitespace. If that does not force the istringstream into an eof state, an exception is thrown. Thus, the integer steps may not be followed by any stray characters (other than whitespace).

std::string s;
if (!std::getline(is, s))
    throw std::runtime_error(
        "`is` failed mid-record while reading field `steps`");
int steps;
std::istringstream iss{ s };
if ((iss >> steps) && !iss.eof())
    iss >> std::ws;
if (iss.fail() || !iss.eof())
    throw std::runtime_error(
        "could not parse field `steps`: \"" + s + '"');

Blank lines

Checking for blank lines is a bit tricky. That's because blank lines are required between records, but not at the end of the file.

There are three cases:

  1. std::getline successfully reads what is supposed to be a blank line. In this case, throw an exception if the line is not blank.
  2. std::getline fails, due to end-of-file. This is not an error. Blank lines are not required at the end of a file. Call is.clear(), to place the stream back into a good state.
  3. std::getline fails for some other reason. This is unexpected. Throw an exception.

Consider this example, which is taken from function load_exercise_plans (above). For this loop to work, ifs must not be in a failed state after reading the last record. That's why we must call is.clear() in case 2. above.

ExercisePlan p;
while (ifs >> p)
    e_plans.push_back(p);

Note: is.clear() clears both eofbit and failbit (as well as badbit). That is good enough for this application.

Slightly more precise would be to clear failbit alone, which can be accomplished using this statement:

is.clear(is.rdstate() & ~std::ios_base::failbit);

I am using is.clear() only to avoid explaining the low-level details of iostate bit masking. See CppReference for more information.

Stream extraction operator

Here is the source code for operator>>:

std::istream& operator>>(std::istream& is, ExercisePlan& plan)
{
    std::string name;
    if (!std::getline(is, name))
    {
        if (!is.eof())
            throw std::runtime_error(
                "`is` failed unexpectedly while reading field `name`.");
        return is;  // eof at the beginning of a record is okay.
    }

    std::string s;
    if (!std::getline(is, s))
        throw std::runtime_error(
            "`is` failed mid-record while reading field `steps`");
    int steps;
    std::istringstream iss{ s };
    if ((iss >> steps) && !iss.eof())
        iss >> std::ws;
    if (iss.fail() || !iss.eof())
        throw std::runtime_error(
            "could not parse field `steps`: \"" + s + '"');

    std::string date;
    if (!std::getline(is >> std::ws, date))
        throw std::runtime_error(
            "`is` failed mid-record while reading field `date`");

    if (std::getline(is, s))
    {
        if (!tbx::is_whitespace_or_empty(s))
            throw std::runtime_error(
                "expecting blank line between records: \"" + s + '"');
    }
    else if (is.eof())
    {
        // Could also use:
        // is.clear(is.rdstate() & ~std::ios_base::failbit);
        is.clear();
    }
    else
    {
        throw std::runtime_error(
            "`is` failed while reading blank line between records.");
    }

    // Note the "strong guarantee." There are no partial updates 
    // to parameter `plan`. If any one of its fields cannot be read
    // from the stream, then NONE of them will be updated. 
    plan.setPlanName(name);
    plan.setGoalSteps(steps);
    plan.setDate(date);
    return is;
}

tbx::is_whitespace_or_empty

In order to verify that the blank lines were, indeed, blank, I used a function tbx::is_whitespace_or_empty.

if (!tbx::is_whitespace_or_empty(s))
    throw std::runtime_error(
        "expecting blank line between records: \"" + s + '"');

tbx::is_whitespace_or_empty is a function from my toolbox that returns true when its argument is empty or else is made up entirely of whitespace characters. It calls function tbx::trim_whitespace_view, another toolbox function, to do the work.

tbx::trim_whitespace_view trims leading and trailing whitespace from its argument, and returns the result as as a std::string_view.

Source code for both follows.

namespace tbx
{
    auto trim_whitespace_view(std::string_view sv) noexcept -> std::string_view
    {
        // Trim leading and trailing whitespace from string_view `sv`.
        auto const first{ sv.find_first_not_of(" \f\n\r\t\v") };
        if (first == std::string_view::npos)
            return {};
        auto const last{ sv.find_last_not_of(" \f\n\r\t\v") };
        enum : std::string_view::size_type { one = 1u };
        return sv.substr(first, (last - first + one));
    }
    bool is_whitespace_or_empty(std::string_view sv) noexcept
    {
        return tbx::trim_whitespace_view(sv).empty();
    }
}