How do I read and parse input from a user that is comma separated by receiving an std::istream object in c++?

天涯浪子 提交于 2021-01-29 10:11:16

问题


I have a class in c++ called Airplane. I need to create a read function using std::istream that lets a user type after a prompt in the console a line that is comma separated. This line of input will then be split up using the commas and assigned to different private data members of the class. As an example, if the user types into the console "abc,12345,hello," then I would need to parse that line and assign abc to one variable, 12345 to another and hello to the last. I believe after the user types in "123,abc,hello," that line is stored somewhere and I can access that using istream somehow?

What I have so far is below:

std::istream& Airplane::read(std::istream& in) {
   if (comma_separated == true) {
   // parse the line inputted by the user and then assign it to 3 variables
   // after getting the input somehow assign to variables
   this->first_var = info_before_first_comma;
   this->second_var = second_comma_text;
   etc...
   }
}

I believe I also need some sort of overload operator function to pass the class to, which then calls the read function above to process the class data. Something possibly like below?

std::istream& operator>>(std::istream& output, Airplane& airplane) {}

That way I could create a class, then call cin >> class_name and it would take in input, process it, and assign it to that classes variables. tldr: i need to read user input from the console and separate the text based on commas, then assign to variables. my confusion is I dont know where to start or how to actually get the "123,abc,hello," line to process from the user. Thank you for reading.

UPDATED INFORMATION The code given below runs (chose example 3), but doesnt give the correct result. I call cin >> classname and input "1234,abcdaef,asdasd," and press enter. Then I call cout << classname and it prints the old data its storing and ignore the input given.

When I try to do the following to check if the tokens are storing data:

            std::cout << token[0] << std::endl;
            std::cout << token[1] << std::endl;
            std::cout << token[2] << std::endl;

I get a debug "vector subscript out of range" error.

This is how I stored the 3 values into my private data members, I have an int and 2 char arrays.

                this->store_int = std::stoi(token[0]);

                this->store_first_char = new char[token[1].length() + 1];
                strcpy(this->store_first_char, token[1].c_str());

                this->store_second_char = new char[token[2].length() + 1];
                strcpy(this->store_second_char, token[2].c_str());

But this didnt work either. One thing I forgot to clarify is that there is a comma at the end always if that matters. Thank you.


回答1:


Let us go step by step.

First, and most important, one complete input line will read using the function std::getline. This function will read a complete line from whatever std::istream and put it into a std::string.

Then, we need to split up the complete string into substrings. The substrings are separated by comma. In the end, we would have an STL container, containing all the substrings.

Then we make a sanity check and look at the number of substrings that we got after splitting up the string. If the count is OK, then we do either store the strings directly, or convert them to the required data type, for example an int or a float.

Since the reading of a line with std::getline is simple, we will first concentrate on splitting up a string. This is also called tokenizing a string.

I will show you several different approaches on how to tokenize a string:

Splitting a string into tokens is a very old task. There are many many solutions available. All have different properties. Some are difficult to understand, some are hard to develop, some are more complex, slower or faster or more flexible or not.

Alternatives

  1. Handcrafted, many variants, using pointers or iterators, maybe hard to develop and error prone.
  2. Using old style std::strtok function. Maybe unsafe. Maybe should not be used any longer
  3. std::getline. Most used implementation. But actually a "misuse" and not so flexible
  4. Using dedicated modern function, specifically developed for this purpose, most flexible and good fitting into the STL environment and algortithm landscape. But slower.

Please see 4 examples in one piece of code.

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <regex>
#include <algorithm>
#include <iterator>
#include <cstring>
#include <forward_list>
#include <deque>

using Container = std::vector<std::string>;
std::regex delimiter{ "," };


int main() {

    // Some function to print the contents of an STL container
    auto print = [](const auto& container) -> void { std::copy(container.begin(), container.end(),
        std::ostream_iterator<std::decay<decltype(*container.begin())>::type>(std::cout, " ")); std::cout << '\n'; };

    // Example 1:   Handcrafted -------------------------------------------------------------------------
    {
        // Our string that we want to split
        std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
        Container c{};

        // Search for comma, then take the part and add to the result
        for (size_t i{ 0U }, startpos{ 0U }; i <= stringToSplit.size(); ++i) {

            // So, if there is a comma or the end of the string
            if ((stringToSplit[i] == ',') || (i == (stringToSplit.size()))) {

                // Copy substring
                c.push_back(stringToSplit.substr(startpos, i - startpos));
                startpos = i + 1;
            }
        }
        print(c);
    }

    // Example 2:   Using very old strtok function ----------------------------------------------------------
    {
        // Our string that we want to split
        std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
        Container c{};

        // Split string into parts in a simple for loop
#pragma warning(suppress : 4996)
        for (char* token = std::strtok(const_cast<char*>(stringToSplit.data()), ","); token != nullptr; token = std::strtok(nullptr, ",")) {
            c.push_back(token);
        }

        print(c);
    }

    // Example 3:   Very often used std::getline with additional istringstream ------------------------------------------------
    {
        // Our string that we want to split
        std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
        Container c{};

        // Put string in an std::istringstream
        std::istringstream iss{ stringToSplit };

        // Extract string parts in simple for loop
        for (std::string part{}; std::getline(iss, part, ','); c.push_back(part))
            ;

        print(c);
    }

    // Example 4:   Most flexible iterator solution  ------------------------------------------------

    {
        // Our string that we want to split
        std::string stringToSplit{ "aaa,bbb,ccc,ddd" };


        Container c(std::sregex_token_iterator(stringToSplit.begin(), stringToSplit.end(), delimiter, -1), {});
        //
        // Everything done already with range constructor. No additional code needed.
        //

        print(c);


        // Works also with other containers in the same way
        std::forward_list<std::string> c2(std::sregex_token_iterator(stringToSplit.begin(), stringToSplit.end(), delimiter, -1), {});

        print(c2);

        // And works with algorithms
        std::deque<std::string> c3{};
        std::copy(std::sregex_token_iterator(stringToSplit.begin(), stringToSplit.end(), delimiter, -1), {}, std::back_inserter(c3));

        print(c3);
    }
    return 0;
}

So, after having an initial string, like "abc,12345,hello", we will now have a container of std::strings, e.g. a std::vector containing the substrings: So, "abc","12345" and "hello".

"abc" and "hello" can directly be stored (assigned to) in a string variable of your class. "12345" must be converted using an existing function, like for example std::stoi, to an int and assigned to a member variable.

The last step is to use all of this in a class (or struct).

This would look for example like this:

struct MyData {
    // Our data
    std::string item1{};
    int value{};
    std::string item2{};
    
    // Overwrite extractor operator
    friend std::istream& operator >> (std::istream& is, MyData& md) {
        if (std::string line{};std::getline(is, line)) {

            // Here we will store the sub strings
            std::vector<std::string> token{};

            // Put in an istringstream for further extraction
            std::istringstream iss{ line };
            
            // Split
            for (std::string part{}; std::getline(iss, part, ','); c.push_back(part))
                ;

            // Sanity check
            if (token.size() == 3) {
            
                // Assigns value to our data members
                md.item1 = token[0];
                md.value = std::stoi(token[1]);
                md.item2 = token[2];
            }

        }
        return is;
    }
};

Sorry to say, but this is uncompiled, not tested code. It shoud give you an idea, on how it could be implemented.

And now you can use std::iostream to get data into your struct.

MyData md;
std::cin >> md;

I hope I could answer your question. If not, then please ask.



来源:https://stackoverflow.com/questions/63126616/how-do-i-read-and-parse-input-from-a-user-that-is-comma-separated-by-receiving-a

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!