3

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;
Daniel
  • 2,318
  • 2
  • 22
  • 53
  • I treat % as a divide by 100 operator in some of my grammars (which also allows 10000%% as 1), but I don't conflate my output formatting and input tokens. – Bathsheba Apr 25 '22 at 19:56
  • Actually *%* is just an example, it can be anything else: my idea of using that was based on the old `printf` style formatting. Do you format your output also with some `spirit`? – Daniel Apr 25 '22 at 20:04
  • Alas not. Where's @sehe when you need them? – Bathsheba Apr 25 '22 at 20:13
  • 1
    @Bathsheba right here. (being very confused™) – sehe Apr 25 '22 at 20:59

1 Answers1

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

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Brilliant solution, thank you! How shall we modify the `formatspec`, to ban these inputs (consider them invalid): `5/3%.3...`, `3*5%`, `3*5%.2%`? So to only allow a strict `%.XY` *formatspec*? – Daniel Apr 26 '22 at 10:38
  • I found out: `("%." >> precision | qi::attr(0u));` does the trick, `qi::attr(0u)` allows to miss the format specifier, right? – Daniel Apr 26 '22 at 10:46
  • 1
    Precisely. I just went by your own examples, which did _not_ require the "strict format spec" – sehe Apr 26 '22 at 11:16
  • Yeah. Thank you. One very tiny bit of information I'm looking for: how can achieve *1.6666666666667* for *5/3* input? I.e. to not apply `std::fixed << std::setprecision(fmt.frac_digits)` if *formatspec* is `qi::attr(0u)`? – Daniel Apr 26 '22 at 11:38
  • I "managed" this as well with `("%." >> precision | qi::attr(10u))` for *formatspec* and "finetune" the printer: `return (fmt.frac_digits == 10 ? os << val : os << std::fixed << std::setprecision...` Do we have a more "clever" approach than this? oh, and also changed `qi::uint_parser` for *precision*, otherwise *10* would make no sense here. – Daniel Apr 26 '22 at 11:54
  • 1
    You'll have to decide on your default format one way or another. To do literally what you said (_"to not apply `std::fixed << std::setprecision(fmt.frac_digits)`"_): https://godbolt.org/z/55cEKW8fj (I also changed `float` to `double` because otherwise you show [more](https://godbolt.org/z/K84cYbM7h) than the available significant digits...) – sehe Apr 26 '22 at 12:33