0

I have an issue that I haven't been able to find a good way to solve, mostly because I am relatively new to C++, but not new to programming. I have a file with several lines in it, one of them being:

Plain Egg 1.45

I need to be able to read that line and split the first part, "Plain Egg", into a string, and then the last part, 1.45, into a float, and then do that for the rest of the lines. The following is some code I have so far, but I have been having an issue where it refuses to even read the file for some reason:

string line;
ifstream menuFile("menu.txt");
if (menuFile.is_open())
{
    int i = 0;
    while (getline(menuFile, line));
    {
        cout << line << endl;

        istringstream iss(line);

        iss >> dataList[i].menuItem >> dataList[i].menuPrice;
        /*iss >> dataList[i].menuPrice;*/
        i++;
    }

}
else
{
    cout << "Unable to open file.";
}

When I run it, it doesn't spit out "Unable to open file.", and when I trace it, it does enter the if loop, but it just doesn't read it. Besides that problem though, I want to know if this code would work in the way I want it to, and if doesn't, how to solve this problem.

EDIT: When I run it, it outputs what the last line of the file said, that being "Tea 0.75". The full file is as follows:

Plain Egg 1.45
Bacon and Egg 2.45
Muffin 0.99
French Toast 1.99
Fruit Basket 2.49
Cereal 0.69
Coffee 0.50 
Tea 0.75

EDIT 2: For some reason, the following code goes straight to the last line, Tea 0.75, and I have no idea why, shouldn't the getline just go line by line until the last line(?):

string line;
int index;
ifstream menuFile("menu.txt");
if (menuFile.is_open())
{
    while (getline(menuFile, line));
    {
        cout << line << endl;
        index = line.find_last_of(' ');
        cout << index << endl;
    }

}

EDIT 3: Above code has a semicolon at the end of the while loop, no wonder it was just ending on the last line, ughhh.

  • Because the protocol uses spaces as a delimiter and also allows spaces in the tokens you need to spend extra effort finding the pieces that are invariant and start with them. In this case, the last token will always be a float. Start at the end of the line and work backwards to parse out the float. The rest will be the string. – user4581301 Jan 21 '22 at 19:52
  • You could solve this with a pretty straight forward std::regex, or a not very difficult custom parsing routine, or a pretty simple Boost Spirit X3 grammar. Depending on your comfort level and needs. – Eljay Jan 21 '22 at 19:55
  • That is an awesome piece of advice, I am going to search it up right now, but what would be the best way to read from the end of the line and back? – Brent Mayes Jan 21 '22 at 19:56
  • [`std::string::find_last_of`](https://en.cppreference.com/w/cpp/string/basic_string/find_last_of) can help you find the last space. Once you have its position, splitting the line is trivial... assuming some tool didn't add a space to the end of the line. – user4581301 Jan 21 '22 at 19:56
  • Should I save the whole line as a string first and then use this? – Brent Mayes Jan 21 '22 at 19:57
  • 1
    @BrentMayes: Yes, the way you are using `getline` is good. You only have to work on the code that is executed after `getline` succeeded. – Andreas Wenzel Jan 21 '22 at 19:58
  • Thank you! How would I use the method of find_last_of to separate the two parts? – Brent Mayes Jan 21 '22 at 19:59
  • 1
    @user4581301 [`std::string::rfind()`](https://en.cppreference.com/w/cpp/string/basic_string/rfind) would be more appropriate in this case than `std::string::find_last_of()`. – Remy Lebeau Jan 22 '22 at 00:32
  • Good point. The price is almost certainly going to be shorter than the name. – user4581301 Jan 22 '22 at 00:32

3 Answers3

3
  • Grab the line into a string.
  • Get the position of the last separator.
  • Your text is a substring of the line until the position of the separator.
  • Your number is a substring of the line from the position of the separator. You'll need to convert it to double first (and you should check for errors).

[Demo]

#include <iostream>  // cout
#include <string>  // find_last_of, getline, stod

int main()
{
    std::string line{};
    while (std::getline(std::cin, line))
    {
        auto pos{line.find_last_of(' ')};
        auto text{line.substr(0, pos)};
        auto number{std::stod(line.substr(pos))};
        std::cout << "text = " << text << ", number = " << number << "\n";
    }
}

// Outputs
//
//   text = Plain Egg, number = 1.45
//   text = Bacon and Egg, number = 2.45
//   text = Muffin, number = 0.99
//   text = French Toast, number = 1.99
//   text = Fruit Basket, number = 2.49
//   text = Cereal, number = 0.69
//   text = Coffee, number = 0.5
//   text = Tea, number = 0.75
//   

A more robust solution taking into account @Dúthomhas' comments:

  • Trims the right hand side of the string before finding the last separator.
  • Catches std::stod exceptions.

This solution detects:

  • Blank lines.
  • Lines without texts.
  • Lines without numbers.
  • Incorrect number formats.

[Demo]

#include <boost/algorithm/string.hpp>
#include <fmt/core.h>
#include <iostream>  // cout
#include <string>  // find_last_of, getline, stod

int main()
{
    std::string line{};
    while (std::getline(std::cin, line))
    {
        try
        {
            boost::trim_right(line);
            auto pos{line.find_last_of(' ')};
            auto text{line.substr(0, pos)};
            auto number{std::stod(line.substr(pos))};
            std::cout << "text = " << text << ", number = " << number << "\n";
        }
        catch (const std::exception&)
        {
            std::cout << fmt::format("* Error: invalid line '{}'\n", line);
        }
    }
}

// Outputs:
//
//   text = Plain Egg, number = 1.45
//   text = Bacon and Egg, number = 2.45
//   text = Muffin, number = 0.99
//   text = French Toast, number = 1.99
//   * Error: invalid line ''
//   * Error: invalid line 'Fruit Basket'
//   * Error: invalid line '0.75'
//   * Error: invalid line 'Coffee blah'
rturrado
  • 7,699
  • 6
  • 42
  • 62
0

You can do this for your given inputs by iterating the string to find the index at which the float value begins. Then grab the substring for each part and cast the value to a float.

#include <iostream>

using namespace std;

int main()
{
    string s = "Bacon and Egg 2.45";
    int l = 0;
    string food;
    float val = 0.0;
    
    for (int i = 0; i < s.length(); i++)
    {
        if(isdigit(s[i]))
        {
            l = i;
            break;
        }
    }
    
    food = s.substr(0,l);
    val = stof(s.substr(l,s.length()));
    cout << food << endl;
    cout << val;
}
MFerguson
  • 1,739
  • 9
  • 17
  • 30
  • 1
    The function call `isdigit(s[i])` will cause undefined behavior for most function arguments that are not representable as an `unsigned char` (i.e. for negative values). See [this answer to another question](https://stackoverflow.com/a/45007070/12149471) for further information and for a fix. – Andreas Wenzel Jan 21 '22 at 20:03
  • I was about to use this method utilizing string_last_of, this seems like a great way to do it. – Brent Mayes Jan 21 '22 at 20:03
  • 1
    Will that work for `"Bacon and 3 Eggs 2.45"` ? :) – Vlad Feinstein Jan 21 '22 at 20:05
  • 1
    No it would not, luckily the only responses I have are the ones listed, but for this reason I am using find_last_of and finding the last space. – Brent Mayes Jan 21 '22 at 20:08
  • 2
    @BrentMayes That is correct. The only invariant you have on that line is that the _last_ item is always the number. So, find the beginning of the last item and split the line there. – Dúthomhas Jan 21 '22 at 20:16
0

Somewhat heavy handed: using the same idea of looking for the last space in string but with trimming spaces left and right. The bad thing is that while it is a lot of code and it still would not work with unicode in any form.

  // Right trim the line                                                                                                                                                                                                                                     
  while(!line.empty()) {
    if (isspace(line.back())) {
      line.pop_back();
    }
  }

  // Find the last space                                                                                                                                                                                                                                     
  size_t pos = line.find_last_of(" \t");
  if (pos == std::string::npos) {
    // Bad line: no spaces or only spaces                                                                                                                                                                                                                    
    handle_it();
  }

  // Get the price                                                                                                                                                                                                                                           
  double price = 0.0;
  try {
    size_t end_pos = 0;
    price = std::stod(line.substr(pos), &end_pos);
    if ((pos + end_pos) != line.length()) {
        // Another bad format: garbage at the end                                                                                                                                                                                                            
        handle_it();
      }
  } catch (...) {
    // Another bad format                                                                                                                                                                                                                                    
    handle_it();
  }

  // Left trim the item                                                                                                                                                                                                                                      
  size_t skip = 0;
  while(skip > pos && isspace(line[skip])) {
    skip++;
  }
  if (skip == pos) {
    // Another bad format: spaces + price                                                                                                                                                                                                                    
    handle_it();
  }

  // Right trim the item                                                                                                                                                                                                                                     
  // we know that we have at leas one non-space                                                                                                                                                                                                              
  pos--;
  while(isspace(line[pos])) {
    pos--;
  }

  std::string item = line.substr(skip, pos + 1);

uuu777
  • 765
  • 4
  • 21
  • 1
    Calling [`std::isspace`](https://en.cppreference.com/w/cpp/string/byte/isspace) with a value that is not representable as an `unsigned char` (e.g. a negative value when using a character code >127) and that is not `EOF` will result in undefined behavior. See [this answer to another question](https://stackoverflow.com/a/45007070/12149471) for further information and for how to fix it. The information regarding `std::isdigit` also applies to `std::isspace`. – Andreas Wenzel Jan 21 '22 at 20:56