1

I would like to define a physical units parser using boost spirit. The parser will have to account for unit and also the prefix. To do this, I have too maps that stores the prefixes and the units in SI which are define below in pseudo-code.

std::map<std::string,double> prefixes;
// prefix - milli
prefixes["m"]=1.0e-03;
// prefix - centi
prefixes["c"]=1.0e-02;
...

std::map<std::string,std::vector<int>> units;
// time - seconds
units["s"] = {1,0,0,0,0,0,0};
// length - meters
units["m"] = {0,1,0,0,0,0,0};
...

These maps are not fixed (especially the later one) in the sense that the user may decide to define new prefixes and units.

I built the following grammar to parse strings such as kg or cm and to get as output a std::pair<double,std::vector<int>> that will contain the prefix and unit that match the successfully parsed string.

UnitParser.h

struct UnitParser : qi::grammar<std::string::const_iterator,std::pair<double,std::vector<int>>()>
{
    UnitParser();

    qi::rule<std::string::const_iterator,std::pair<double,std::vector<int>>()> prefixedUnitRule;
    qi::rule<std::string::const_iterator,double()> prefixRule;
    qi::rule<std::string::const_iterator,std::vector<int>()> unitRule;
};

and its cpp counterpart UnitParser.cpp:

UnitParser::UnitParser() : UnitParser::base_type(prefixedUnit)
{
    using namespace qi;
    using namespace phx;

    prefixedUnitRule = prefixRule >> unitRule;

    for (const auto& p : prefixes)
        prefixRule = string(p.first)[_val=p.second] | prefixRule.copy()[_val=p.second];

    for (const auto& p : units)
        unitRule = string(p.first)[_val=p.second] | unitRule.copy()[_val=p.second];

}

This implementation compiles but produces wrong results.

My question/problem is the following, how to build the prefixRule and unitRule rules using a loop over the prefixes and units maps ?

sehe
  • 374,641
  • 47
  • 450
  • 633
Eurydice
  • 8,001
  • 4
  • 24
  • 37
  • Composing rules [dynamically](https://stackoverflow.com/a/20167382/85371) and dealing with temporary parser expressions is fraught with danger. If you want to go down this route, be sure to look at Spirit X3. – sehe Sep 19 '16 at 19:40

1 Answers1

4

You seem to be looking for qi::symbols<>:

template <typename It>
struct parser : qi::grammar<It, parsed_unit()> {
    parser() : parser::base_type(start) {
        _prefix.add
            ("m",  1.0 * std::milli::num / std::milli::den)
            ("c",  1.0 * std::centi::num / std::centi::den)
            ("d",  1.0 * std::deci::num / std::deci::den)
            ("da", 1.0 * std::deca::num / std::deca::den)
            ("h",  1.0 * std::hecto::num / std::hecto::den)
            ("k",  1.0 * std::kilo::num / std::kilo::den);
        _unit.add
            ("s", units::s)
            ("m", units::m);

        start = _prefix       >> _unit >> qi::eoi
              | qi::attr(1.0) >> _unit >> qi::eoi;
    }
  private:
    qi::symbols<char, std::reference_wrapper<units::unit const> > _unit;
    qi::symbols<char, double> _prefix;
    qi::rule<It, parsed_unit()> start;
};

A full demo using std::array for the dimension and some traits so we can efficiently transform from a reference to dimension to the parsed unit:

DEMO

Live On Coliru

#include <boost/fusion/adapted/std_pair.hpp>
#include <boost/spirit/include/qi.hpp>
#include <ratio>

namespace qi = boost::spirit::qi;

namespace units {
    using unit = std::array<int, 6>;
    static const unit s = {{ 1, 0, 0, 0, 0, 0} };
    static const unit m = {{ 0, 1, 0, 0, 0, 0} };
}

namespace boost { namespace spirit { namespace traits {

    template <> struct is_container<units::unit, void> : mpl::false_ { };

    template <typename T>
        struct assign_to_attribute_from_value<typename std::reference_wrapper<T const>, T, void> :
            assign_to_attribute_from_value<T, T, void> { };

} } }

using parsed_unit = std::pair<double/* factor*/, units::unit/* dimension*/>;

template <typename It>
struct parser : qi::grammar<It, parsed_unit()> {
    parser() : parser::base_type(start) {
        _prefix.add
            ("m",  1.0 * std::milli::num / std::milli::den)
            ("c",  1.0 * std::centi::num / std::centi::den)
            ("d",  1.0 * std::deci::num / std::deci::den)
            ("da", 1.0 * std::deca::num / std::deca::den)
            ("h",  1.0 * std::hecto::num / std::hecto::den)
            ("k",  1.0 * std::kilo::num / std::kilo::den);
        _unit.add
            ("s", units::s)
            ("m", units::m);

        start = _prefix       >> _unit >> qi::eoi
              | qi::attr(1.0) >> _unit >> qi::eoi;
    }
  private:
    qi::symbols<char, std::reference_wrapper<units::unit const> > _unit;
    qi::symbols<char, double> _prefix;
    qi::rule<It, parsed_unit()> start;
};

int main() {
    using It = std::string::const_iterator;
    parser<It> p;
    for (std::string const input : { 
            "mm", "cm", "dm", "m", "dam", "hm", "km",
            "ms", "cs", "ds", "s", "das", "hs", "ks",
        })
    {
        std::cout << "--- Test: '" << input << "'\n";
        auto f = input.begin(), l = input.end();

        parsed_unit u;

        bool ok = qi::phrase_parse(f, l, p, qi::space, u);
        if (ok) {
            std::cout << "Parsed: {";
            std::copy(u.second.begin(), u.second.end(), std::ostream_iterator<int>(std::cout));
            std::cout << "}, scale factor: e" << std::log10(u.first) << " (~" << u.first << ")\n";
        }
        else
            std::cout << "Parse failed\n";

        if (f != l)
            std::cout << "Remaining input: '" << std::string(f,l) << "'\n";
    }
}

Prints

--- Test: 'mm'
Parsed: {010000}, scale factor: e-3 (~0.001)
--- Test: 'cm'
Parsed: {010000}, scale factor: e-2 (~0.01)
--- Test: 'dm'
Parsed: {010000}, scale factor: e-1 (~0.1)
--- Test: 'm'
Parsed: {010000}, scale factor: e0 (~1)
--- Test: 'dam'
Parsed: {010000}, scale factor: e1 (~10)
--- Test: 'hm'
Parsed: {010000}, scale factor: e2 (~100)
--- Test: 'km'
Parsed: {010000}, scale factor: e3 (~1000)
--- Test: 'ms'
Parsed: {100000}, scale factor: e-3 (~0.001)
--- Test: 'cs'
Parsed: {100000}, scale factor: e-2 (~0.01)
--- Test: 'ds'
Parsed: {100000}, scale factor: e-1 (~0.1)
--- Test: 's'
Parsed: {100000}, scale factor: e0 (~1)
--- Test: 'das'
Parsed: {100000}, scale factor: e1 (~10)
--- Test: 'hs'
Parsed: {100000}, scale factor: e2 (~100)
--- Test: 'ks'
Parsed: {100000}, scale factor: e3 (~1000)
sehe
  • 374,641
  • 47
  • 450
  • 633
  • Your answer matches exactly my request and has been very helpful. Thanks. – Eurydice Sep 20 '16 at 09:31
  • @PellegriniEric In that case you should probably upvote the answer. – llonesmiz Sep 20 '16 at 21:06
  • 1
    I thought that I did. Done now – Eurydice Sep 21 '16 at 08:35
  • off topic: Do you remember lately discussed a possible alternative approach to define rules/grammar using variable templates regarding x3? – Tomilov Anatoliy Oct 06 '16 at 08:19
  • @Orient come again? If you simply mean you can have variable templates, sure. I often combine that with lambdas: `template auto foo = [](auto& ctx) { .... };`. Not sure what you are getting at in the current context. – sehe Oct 06 '16 at 08:39
  • @sehe I just mean, that you also follow news related to boost spirit (mailing lists, SO etc) like me. And you may remember recent discussion about some proposal of using variable templates in x3 for grammar definition. I can't find where it was and ask help from you. It is not related to the context of this question at all. Just a place to ask you (I have no contact with you). – Tomilov Anatoliy Oct 06 '16 at 11:28
  • Ah. Next time, consider asking [in the Lounge](http://chat.stackoverflow.com/rooms/10/loungec). Moving there now – sehe Oct 06 '16 at 12:17