Search code examples
c++csvparsingboost-spiritboost-phoenix

Parsing simple csv table with boost-spirit


I was trying to use boost-spirit to parse a fairly simple cvs file format.

My csv file looks like this:

Test.txt

2
5. 3. 2.
6. 3. 6.

The first integer represents the number of lines to read and each line consists of exactly three double values.

This is what I got so far.

main.cpp

#include <vector>
#include <fstream>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix_core.hpp>
#include <boost/spirit/include/phoenix_operator.hpp>
#include <boost/spirit/include/phoenix_stl.hpp>
#include <boost/phoenix/object/construct.hpp>

std::vector<std::vector<double>> parseFile(std::string& content)
{
    std::vector<std::vector<double>> ret;
    using namespace boost::phoenix;
    using namespace boost::spirit::qi;
    using ascii::space;

    int no;
    phrase_parse(content.begin(), content.end(),
        int_[ref(no) = _1] >> repeat(ref(no))[(double_ >> double_ >> double_)[
            std::cout << _1 << _2 << _3
        ]], space
    );
    return ret;
}

int main(int arg, char **args) {
    auto ifs = std::ifstream("Test.txt");
    std::string content((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
    parseFile(content);
}

Now instead of the line std::cout << _1 << _2 << _3 I'm need of something that appends a std::vector<double> containing the three values.

I already tried _val=construct<std::vector<double>>({_1,_2,_3}), but it is not working. So what I'm doing wrong?


Solution

  • It's much simpler than you think¹

    std::vector<std::vector<double>> parseFile(std::string const& content) {
        namespace px = boost::phoenix; 
        using namespace boost::spirit::qi;
    
        int no;
        std::vector<std::vector<double>> data;
    
        bool ok = phrase_parse(content.begin(), content.end(),
            int_ [ px::ref(no) = _1 ] >> eol 
            >> repeat(px::ref(no)) [ repeat(3) [double_] >> (eol|eoi)],
            blank,
            data
        );
    
        if (!ok)
            throw std::runtime_error("Parse failure");
    
        return data;
    }
    

    See it Live On Coliru. It uses automatic attribute propagation - the single most useful feature of Spirit to begin with - and the blank skipper instead of space so we can still parse eol

    Now, I'd suggest to make it even simpler:

    Live On Coliru

    bool ok = phrase_parse(
        content.begin(), content.end(),
        int_ >> eol >> *(+double_ >> (eol|eoi)) >> *eol >> eoi, 
        blank, 
        no, data
    );
    
    if (!ok && (no == data.size()))
        throw std::runtime_error("Parse failure");
    

    Or, in fact, even simpler using just the standard library:

    Live On Coliru

    #include <vector>
    #include <iostream>
    #include <fstream>
    #include <iterator>
    
    std::vector<std::vector<double>> parseFile(std::string const& fname) {
        std::vector<std::vector<double>> data;
    
        auto ifs = std::ifstream(fname);
    
        size_t no = -1;
        if (ifs >> no && ifs.ignore(1024, '\n')) {
            double a, b, c;
            while (ifs >> a >> b >> c)
                data.push_back({a, b, c});
        }
    
        if (!(ifs.eof() && (no == data.size())))
            throw std::runtime_error("Parse failure");
    
        return data;
    }
    
    int main() {
        for (auto& row : parseFile("input.txt"))
            std::copy(row.begin(), row.end(), std::ostream_iterator<double>(std::cout << "\n", " "));
    }
    

    All of them successfully parsing and printing:

    5 3 2 
    6 3 6 
    

    ¹ Boost Spirit: "Semantic actions are evil"?

    Bonus Take: Auto Custom Attribute Types

    Instead of opaque vectors, why don't you use a struct like

    struct Point {
        double x,y,z;
    };
    

    And parse into a std::vector<Point>. As a bonus you get output and parser basically for free:

    BOOST_FUSION_ADAPT_STRUCT(Point, x, y, z)
    
    // parser:
    auto_ >> eol >> *(auto_ >> (+eol|eoi)) >> eoi,
    

    See it Live On Coliru