Search code examples
c++boostboost-spiritboost-spirit-qi

Boost::spirit parsing a float and also formatting it?


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;

Solution

  • 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"?