Home > front end >  Boost::spirit parsing a float and also formatting it?
Boost::spirit parsing a float and also formatting it?

Time:04-27

I have a very cool float calculator implementation with boost::spirit.

It works on a boost::spirit::qi::float_ by default: it gets an std::string input, and calculates the result float of the expression.

See it in action here.

Here is the code for reference:

namespace calc {
    namespace qi = boost::spirit::qi;
    namespace ascii = boost::spirit::ascii;

    ///////////////////////////////////////////////////////////////////////////
    //  Our calculator grammar
    ///////////////////////////////////////////////////////////////////////////
    template <typename Iterator>
    struct calculator : qi::grammar<Iterator, float(), ascii::space_type>
    {
        calculator() : calculator::base_type(expression)
        {
            using qi::_val;
            using qi::_1;
            using qi::float_;

            expression =
                term                            [_val = _1]
                >> *(   (' ' >> term            [_val  = _1])
                    |   ('-' >> term            [_val -= _1])
                    )
                ;

            term =
                factor                          [_val = _1]
                >> *(   ('*' >> factor          [_val *= _1])
                    |   ('/' >> factor          [_val /= _1])
                    )
                ;

            factor =
                float_                          [_val = _1]
                |   '(' >> expression           [_val = _1] >> ')'
                |   ('-' >> factor              [_val = -_1])
                |   (' ' >> factor              [_val = _1])
                ;
        }

        qi::rule<Iterator, float(), ascii::space_type> expression, term, factor;
    };
}


typedef calc::calculator<std::string::const_iterator> calculator;
int main()
{
    calculator calc;
    std::string expression = "3*5";
    float result = 0;

    std::string::const_iterator iter = expression.begin();
    std::string::const_iterator end = expression.end();
                
    std::stringstream resultstream;
    bool r = boost::spirit::qi::phrase_parse(iter, end, calc, boost::spirit::ascii::space, result);
    if (! (r && iter == end)) {
        result = 0;
    }

    resultstream.clear();
    resultstream << result;

    std::cout << "Result: " << resultstream.str() << std::endl;
}

It calculates the expression's value into theresultstream.

Works perfectly, for 3*5 it outputs:

Result: 15

If I change the expression to "5/3" it outputs:

Result: 1.66667

My desire is to always have a fixed number of digits:

For 3*5:

Result: 15.0

For 5/3:

Result: 1.7

I know: adding std::setw to cout solve this. But my goal is different (!):

I want to get the above formatted result into the resultstream, directly from the parser.

My idea is to allow the parser to parse more complex inputs like:

3*5%.1  => 15.0
3*5%.2  => 15.00
3*5%    => 15%
3*5%.2% => 15.00%

How shall I achieve this? Is it worth changing the calculator itself, or it's too heavy and I should prefer some other text processing techniques to parse the required formatting and still do it with std::setw like this:

resultstream << setw(required_width) << result;

CodePudding user response:

My idea is to allow the parser to parse more complex inputs like:

3*5%.1  => 15.0
3*5%.2  => 15.00
3*5%    => 15%
3*5%.2% => 15.00%

This tells me you're not so much creating an expression evaluator, but rather making a format specification. I'm with others that say: separate your concerns.

For what it's worth setw doesn't help you, but std::fixed and std::setprecision might. Regardless, anything C can do, can also happen in a semantic action, so, this hellish contraption should work¹:

using calculator = calc::calculator<std::string::const_iterator>;
static calculator const calc;

for (std::string const expr : {"3*5", "5/3"}) {
    std::stringstream result;

    if (!parse(begin(expr), end(expr), (calc >> qi::eoi) //
           [px::ref(result) << std::fixed << std::setprecision(1) << qi::_1])) {
        result << "#ERROR"; // TODO FIXME error handling
    }

    std::cout << "Result: " << result.str() << std::endl;
}

See it Live On Compiler Explorer, printing:

Result: 15.0
Result: 1.7

BONUS

Regarding the intro dreams:

How shall I achieve this? Is it worth changing the calculator itself, or it's too heavy and I should prefer some other text processing techniques to parse the required formatting and still do it with std::setw like this:

It's not worth changing the calculator, because it isn't a calculator. It really wasn't, and certainly not anymore once you extend your grammar with formatting things (i.e. non-expression things).

You can of course create such a grammar. Let's describe the AST:

using Result = float;

namespace Formatting {
    struct Format {
        unsigned    frac_digits;
        std::string suffix_literal;
    };

    struct FormattedResult {
        Result value;
        Format spec;

        friend std::ostream& operator<<(std::ostream& os, FormattedResult const& fr) {
            auto& [val, fmt] = fr;
            boost::io::ios_all_saver state(os);
            return os << std::fixed << std::setprecision(fmt.frac_digits) << val << fmt.suffix_literal;
        }
    };
}

Now, we can make the toplevel rule return FormmattedResult instead of just Result (i.e. float):

formatspec =
    ("%." >> precision | qi::attr(0u)) >> qi::raw[*qi::char_];

start = qi::skip(qi::space)[expression >> formatspec];

With some additional declarations:

using Skipper = qi::space_type;
qi::rule<Iterator, FormattedResult()> start;
qi::rule<Iterator, Result(), Skipper> expression, term, factor;

// lexemes:
qi::rule<Iterator, Format()>        formatspec;
qi::real_parser<Result>             number;
qi::uint_parser<unsigned, 10, 1, 2> precision;

See it Live On Compiler Explorer

//#define BOOST_SPIRIT_DEBUG
#include <boost/spirit/include/qi.hpp>
#include <boost/phoenix.hpp>
#include <boost/io/ios_state.hpp>
#include <iostream>
#include <iomanip>

namespace px = boost::phoenix;
namespace qi = boost::spirit::qi;

using Result = float;

namespace Formatting {
    struct Format {
        unsigned    frac_digits;
        std::string suffix_literal;
    };

    struct FormattedResult {
        Result value;
        Format spec;

        friend std::ostream& operator<<(std::ostream& os, FormattedResult const& fr) {
            auto& [val, fmt] = fr;
            boost::io::ios_all_saver state(os);
            return os << std::fixed << std::setprecision(fmt.frac_digits) << val << fmt.suffix_literal;
        }
    };
}

BOOST_FUSION_ADAPT_STRUCT(Formatting::Format, frac_digits, suffix_literal)
BOOST_FUSION_ADAPT_STRUCT(Formatting::FormattedResult, value, spec)

namespace Parsers {
    using namespace Formatting;

    template <typename Iterator>
    struct FormattedExpression : qi::grammar<Iterator, FormattedResult()> {
        FormattedExpression() : FormattedExpression::base_type(start) {
            using qi::_1;
            using qi::_val;

            expression =
                term                   [_val = _1]
                >> *(   (' ' >> term   [_val  = _1])
                    |   ('-' >> term   [_val -= _1])
                    )
                ;

            term =
                factor                 [_val = _1]
                >> *(   ('*' >> factor [_val *= _1])
                    |   ('/' >> factor [_val /= _1])
                    )
                ;

            factor =
                number                 [_val = _1]
                |   '(' >> expression  [_val = _1] >> ')'
                |   ('-' >> factor     [_val = -_1])
                |   (' ' >> factor     [_val = _1])
                ;

            formatspec =
                ("%." >> precision | qi::attr(0u)) >> qi::raw[*qi::char_];

            start = qi::skip(qi::space)[expression >> formatspec];

            BOOST_SPIRIT_DEBUG_NODES((start)(expression)(
                term)(factor)(formatspec))
        }

    private:
        using Skipper = qi::space_type;
        qi::rule<Iterator, FormattedResult()> start;
        qi::rule<Iterator, Result(), Skipper> expression, term, factor;

        // lexemes:
        qi::rule<Iterator, Format()>        formatspec;
        qi::real_parser<Result>             number;
        qi::uint_parser<unsigned, 10, 1, 2> precision;
    };
}

int main() {
    using Parser = Parsers::FormattedExpression<std::string::const_iterator>;
    static Parser const parser;

    for (std::string const expr :
        {
            "3*5",       //
            "5/3",       //
            "5/3%.1",    //
            "5/3%.3...", //
            "3*5%.1",    // => 15.0
            "3*5%.2",    // => 15.00
            "3*5%",      // => 15%
            "3*5%.2%",   // => 15.00%
        })               //
    {
        Formatting::FormattedResult fr;
        if (parse(begin(expr), end(expr), parser >> qi::eoi, fr)) {
            std::cout << std::left //
                    << "Input: " << std::setw(12) << std::quoted(expr)
                    << "Result: " << fr << "\n";
        } else {
            std::cout << std::left //
                    << "Input: " << std::setw(12) << std::quoted(expr)
                    << "Parse Error\n";
        }
    }
}

Prints

Input: "3*5"       Result: 15
Input: "5/3"       Result: 2
Input: "5/3%.1"    Result: 1.7
Input: "5/3%.3..." Result: 1.667...
Input: "3*5%.1"    Result: 15.0
Input: "3*5%.2"    Result: 15.00
Input: "3*5%"      Result: 15%
Input: "3*5%.2%"   Result: 15.00%

¹ I hope I didn't accidentally let my personal preference shine through, but see e.g. Boost Spirit: "Semantic actions are evil"?

  • Related