Search code examples
c++boostboost-spirit-qi

Using boost::spirit to parse named parameters in any order


I am writing a parser for a type of input file. The input file looks something like:

[CalculationBlock]
CalculationTitle="Test Parser Input System" , MatchingRadius=25.0, StepSize=0.01,ProblemType=RelSchroedingerEqn
MaxPartialWaveJ=800, SMatConv=10E-8
PartialWaveConv= 10E-8, SmallValueLimit = 10E-8
PotentialRadType=HeavyIon
[end]

Essentially it is divided into blocks that start with [BlockName] and then have a set of named parameters within. The named parameters can be separated by ',' or '\n' characters.

Using the incomplete input file I gave above, I wanted to write a parser for it that would serve as a jumping off point for a more complete input file. I did so but the parser has a weakness that I am not sure how to address. It is not parameter order independent. For example, if a user were to put the parameter PartialWaveConv= 10E-8 before SMatConv=10E-8 it would fail.

I briefly contemplated enumerating each possible order of parameters in a block but I discarded it since there are n! permutations of n parameter value pairs. So my question is: Is there any way to make the parser independent of parameter ordering?

The toy parser I wrote is below, I apologize if it is amateurish, this is my first foray into boost, let alone boost.spirit.

#include<string>
#include<iostream>
#include<cstdlib>
#include<fstream>
#include<boost/config/warning_disable.hpp>
#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_object.hpp>
#include<boost/fusion/include/adapt_struct.hpp>
#include<boost/fusion/include/io.hpp>
#include<boost/spirit/include/support_istream_iterator.hpp>

namespace blocks
{
namespace qi = boost::spirit::qi;
namespace ascii = boost::spirit::ascii;
namespace phoenix = boost::phoenix;

struct CalcBlock
{
    std::string calculationTitle;
    float matchingRad;
    float stepSize;
    std::string problemType;
    int maxPartialWaveJ;
    float sMatrixConvergenceValue;
    float partialWaveConvergenceValue;
    float smallValueLimit;
    std::string potentialRadType;
};

}

//tell fusion about the block structure
BOOST_FUSION_ADAPT_STRUCT(blocks::CalcBlock,
                        (std::string, calculationTitle)
                        (float, matchingRad)
                        (float, stepSize)
                        (std::string, problemType)
                        (int, maxPartialWaveJ)
                        (float, sMatrixConvergenceValue)
                        (float, partialWaveConvergenceValue)
                        (float, smallValueLimit)
                        (std::string, potentialRadType)
)

namespace blocks
{

template <typename Iterator>
struct CalcBlockParser : qi::grammar<Iterator, CalcBlock(), boost::spirit::ascii::blank_type>
{
    CalcBlockParser() : CalcBlockParser::base_type(start)
    {
        using qi::int_;
        using qi::lit;
        using qi::float_;
        using qi::lexeme;
        using ascii::char_;

        quotedString %= lexeme['"' >> +(char_ - '"' - '\n') >> '"'];
        plainString %= lexeme[ +(char_ - ' ' - ',' - '\n') ];

        start %=
            lit("[CalculationBlock]") >> '\n'
            >> lit("CalculationTitle") >> '=' >> quotedString >> (lit(',') | lit('\n'))
            >> lit("MatchingRadius") >> '=' >> float_ >> (lit(',') | lit('\n'))
            >> lit("StepSize") >> '=' >> float_ >> (lit(',') | lit('\n'))
            >> lit("ProblemType") >> '=' >> plainString >> (lit(',') | lit('\n'))
            >> lit("MaxPartialWaveJ") >> '=' >> int_ >> (lit(',') | lit('\n'))
            >> lit("SMatConv") >> '=' >> float_ >> (lit(',') | lit('\n'))
            >> lit("PartialWaveConv") >> '=' >> float_ >> (lit(',') | lit('\n'))
            >> lit("SmallValueLimit") >> '=' >> float_ >> (lit(',') | lit('\n'))
            >> lit("PotentialRadType") >> '=' >> plainString
            >> lit("\n[end]\n");
    }

    qi::rule<Iterator, std::string(), boost::spirit::ascii::blank_type> quotedString;
    qi::rule<Iterator, std::string(), boost::spirit::ascii::blank_type> plainString;
    qi::rule<Iterator, CalcBlock(), boost::spirit::ascii::blank_type> start;
};

}

using std::cout;
using std::endl;
namespace spirit = boost::spirit;
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "\nUsage:\n\t./echos InputFileName\n" << endl;
        return EXIT_FAILURE;
    }

    std::string inputFileName(argv[1]);
    cout << "Reading input from the file: " << inputFileName << endl;
    std::ifstream input(inputFileName);
    input.unsetf(std::ios::skipws);

    spirit::istream_iterator start(input);
    spirit::istream_iterator stop;

    typedef blocks::CalcBlockParser<spirit::istream_iterator> CalcBlockParser;

    CalcBlockParser cbParser;

    blocks::CalcBlock cb;

    bool success = phrase_parse(start, stop, cbParser, boost::spirit::ascii::blank, cb);

    if (success && start == stop)
    {
        std::cout << boost::fusion::tuple_open('[');
        std::cout << boost::fusion::tuple_close(']');
        std::cout << boost::fusion::tuple_delimiter(", ");

        std::cout << "-------------------------\n";
        std::cout << "Parsing succeeded\n";
        std::cout << "got: " << boost::fusion::as_vector(cb) << std::endl;
        std::cout << "\n-------------------------\n";
    }
    else
    {
        std::cout << boost::fusion::tuple_open('[');
        std::cout << boost::fusion::tuple_close(']');
        std::cout << boost::fusion::tuple_delimiter(", ");

        std::cout << "-------------------------\n";
        std::cout << "Parsing failed\n";
        std::cout << "got: " << boost::fusion::as_vector(cb) << std::endl;
        std::cout << "\n-------------------------\n";
    }

    return EXIT_SUCCESS;
}

Solution

  • Just for fun/completeness I reviewed the grammar and came up with the following test.

    I have made a few improvement suggestions left and right (as the OP witnessed on the live stream), and the resulting code, test and output are here:

    Live On Coliru

    #include <boost/fusion/include/adapt_struct.hpp>
    #include <boost/spirit/include/qi.hpp>
    #include <fstream>
    #include <iostream>
    
    namespace blocks {
        struct CalcBlock {
            std::string calculationTitle;
            float       matchingRad;
            float       stepSize;
            std::string problemType;
            int         maxPartialWaveJ;
            float       sMatrixConvergenceValue;
            float       partialWaveConvergenceValue;    
            float       smallValueLimit;
            std::string potentialRadType;
        };
    }
    
    BOOST_FUSION_ADAPT_STRUCT(blocks::CalcBlock, // Boost 1.58+ style adapt-struct
            calculationTitle, matchingRad, stepSize, problemType, maxPartialWaveJ,
            sMatrixConvergenceValue, partialWaveConvergenceValue, smallValueLimit,
            potentialRadType)
    
    namespace blocks {
    
        namespace qi = boost::spirit::qi;
    
        template <typename Iterator>
        struct CalcBlockParser : qi::grammar<Iterator, CalcBlock()> {
    
            CalcBlockParser() : CalcBlockParser::base_type(start) {
    
                using namespace qi;
                auto eol_ = copy((',' >> *eol) | +eol); // http://stackoverflow.com/a/26411266/85371 (!)
    
                quotedString = '"' >> +~char_("\"\n") >> '"';
                plainString  =  +~char_(" ,\n");
    
                start        = skip(blank) [cbRule];
    
                cbRule       = lexeme["[CalculationBlock]"] >> eol 
                  >> (
                          (lexeme["CalculationTitle"] >> '=' >> quotedString >> eol_)
                        ^ (lexeme["MatchingRadius"]   >> '=' >> float_       >> eol_)
                        ^ (lexeme["StepSize"]         >> '=' >> float_       >> eol_)
                        ^ (lexeme["ProblemType"]      >> '=' >> plainString  >> eol_)
                        ^ (lexeme["MaxPartialWaveJ"]  >> '=' >> int_         >> eol_)
                        ^ (lexeme["SMatConv"]         >> '=' >> float_       >> eol_)
                        ^ (lexeme["PartialWaveConv"]  >> '=' >> float_       >> eol_)
                        ^ (lexeme["SmallValueLimit"]  >> '=' >> float_       >> eol_)
                        ^ (lexeme["PotentialRadType"] >> '=' >> plainString  >> eol_)
                     )
                 >> lexeme["[end]"]
                 >> *eol 
                 >> eoi;
            }
    
          private:
            qi::rule<Iterator, CalcBlock()> start;
            qi::rule<Iterator, CalcBlock(), qi::blank_type> cbRule;
            // lexemes:
            qi::rule<Iterator, std::string()> quotedString, plainString;
        };
    }
    
    using   boost::fusion::as_vector;
    typedef boost::spirit::istream_iterator It;
    
    int main(int argc, char **argv) {
        if (argc != 2) {
            std::cout << "Usage:\n\t" << argv[0] << " InputFileName" << std::endl;
            return 1;
        }
    
        std::string inputFileName(argv[1]);
        std::cout << "Reading input from the file: " << inputFileName << std::endl;
        std::ifstream input(inputFileName);
        input.unsetf(std::ios::skipws);
    
        It start(input), stop;
    
        blocks::CalcBlock cb;
        blocks::CalcBlockParser<It> cbParser;
    
        bool success = parse(start, stop, cbParser, cb);
    
        {
            using namespace boost::fusion;
            std::cout << tuple_open('[') << tuple_close(']') << tuple_delimiter(", ");
        }
    
        std::cout << "-------------------------\n";
        std::cout << "Parsing " << (success?"succeeded":"failed") << "\n";
        std::cout << "got: "    << as_vector(cb)                  << "\n";
        std::cout << "-------------------------\n";
    }
    

    Input:

    [CalculationBlock]
    CalculationTitle="Test Parser Input System"
    
    
    SMatConv=10E-8,
    
    
    PartialWaveConv= 10E-8, MaxPartialWaveJ=800, SmallValueLimit = 10E-8
    
    PotentialRadType=HeavyIon , MatchingRadius=25.0, StepSize=0.01,ProblemType=RelSchroedingerEqn
    
    [end]
    

    Output:

    Reading input from the file: input.txt
    -------------------------
    Parsing succeeded
    got: [Test Parser Input System, 25, 0.01, RelSchroedingerEqn, 800, 1e-07, 1e-07, 1e-07, HeavyIon]
    -------------------------