Generating text with Spirit Karma

Spirit Karma is the counterpart of Spirit Qi. Qi is used to parse a string using a specified grammar to extract data and put it in internal data structure, while Karma accepts data in internal format and generates a string from them accordingly to the specified grammar.

If you have no Karma at hand, you would probably use sprintf() or std::stringstream to perform the same task. But Karma gives you more flexibility, keeping the code easy to read and maintain, producing code that is usually faster. Even code size should not be an issue. And, if you already use Spirit Qi, Spirit Karma comes out quite naturally. On the negative side, Spirit code generation is not that easy, given the complex structure of the framework, and this could slow down a bit the project build time.

From double to string.

Using Karma to put a double in a string looks a bit of overkilling. But also using standard C++ the resulting code is not the simplest piece of code on Planet Earth. Typically I come out with something like this:
std::string d2str(double d)
{
  std::ostringstream oss;
  oss << d;
  return std::move(oss.str());
}
A standard stringstream object is used as mediator from internal type (double in this case) and a string.

Here is a way to get the same result using Karma:
bool kGenerate1(double d, std::string& s)
{
  using boost::spirit::karma::double_;
  using boost::spirit::ascii::space;
  using boost::spirit::karma::generate;

  std::back_insert_iterator<std::string> sink(s);
  return generate(sink, double_, d);
}
Basically, all the job resolves in calling karma::generate() passing an insert iterator to the collection where we want to push the result (typically a string), the grammar that has to be used (here just double_), and the value that has to be used to generate the result.

Lot of namespaces, but the code itself is even clearer than the one that uses standard functionality. And it should be faster too.

From two double to string.

If the doubles in input are two we have to face a couple of issue. How to pass them to Karma, and how to insert a delimiter between them. First issue could be solved passing explicitly both values to the generator [see comment to this post for details], or using an STL container that contains both of them; and the second issue calling karma::generate_delimited(), that accepts as input a delimiter and a skipper sequence:
bool kGenerate2(const double d1, const double d2, std::string& s) // 1
{
  using boost::spirit::karma::double_;
  using boost::spirit::ascii::space;
  using boost::spirit::karma::generate_delimited;

  std::back_insert_iterator<std::string> sink(s);
  return generate_delimited(sink, double_ << ',' << double_, space, d1, d2); // 2
}

bool kGenerate2(const std::deque<double>& dd, std::string& s) // 3
{
  using boost::spirit::karma::double_;
  using boost::spirit::ascii::space;
  using boost::spirit::karma::generate_delimited;

  std::back_insert_iterator<std::string> sink(s);
  return generate_delimited(sink, double_ << ',' << double_, space, dd);
}
1. First overload for our generating function: it explicitly requires two double values.
2. The grammar could be read as: a double followed by a comma, followed by a second double. Notice there the use of the "put to" operator (<<) that differs from Qi, where the "get from" (>>) is used. As a skipper is used the blank character.
3. Second overload, here I used std::deque as container for the values to be used.

One or more doubles to string.

Let's rewrite the previous case in a more generic way. Now we expect the doubles in input to be one or more, and we leave the user the choice of the used container for them. The grammar now specify that we expect a double followed by zero or more blocks made of a comma followed by a double:
template <typename Container>
bool kGenerate(const Container& c, std::string& s)
{
  using boost::spirit::karma::double_;
  using boost::spirit::ascii::space;
  using boost::spirit::karma::generate_delimited;

  std::back_insert_iterator<std::string> sink(s);
  return generate_delimited(sink, double_ << *(',' << double_), space, c);
}

Output as double or as int

As Nikhil pointed out, my d2str() function above behaves differently to kGenerate1(). The standard stringstream class do not output the fractional part of a double if the passed value has an actual integral internal representation, while Karma relies on the type the user specifies to decide the format to use. I had told it to always use the double_ format, and so I always get a double in output.

If we want our code to mimic the default stringstream behaviour, we could check explicitly on our own the fractional part of the double value. Something like this:
bool kGenerate1b(double d, std::string& s)
{
  using boost::spirit::karma::double_;
  using boost::spirit::karma::int_;
  using boost::spirit::ascii::space;
  using boost::spirit::karma::generate;

  std::back_insert_iterator sink(s);

  double asInt;
  if(std::modf(d, &asInt) == 0.0) // 1
    return generate(sink, int_, d); // 2
  else
    return generate(sink, double_, d);
}

1. The standard C math library provides this useful function modf() that split a double value in its integral and fractional parts. It is just what we need. Only if the fractional part, as returned by modf(), is zero (as double) we are going to dump the input value formatted as a double.
2. When the double in input is actually an integer, I use the karma::int_ format instead.

I based this post on a C++ source file provided by the original Boost Spirit Karma documentation.

5 comments:

  1. A little know feature of generators (btw, of parsers as well) is the ability to pass more than one attribute to the API functions if sequences are involved. This allows to rewrite your example above as:


    generate(sink, double_ << ',' << double_, space, d1, d2);

    ReplyDelete
  2. Thank you for the useful remark, I have update the post accordingly.

    ReplyDelete
  3. Thanks for sharing your knowledge through these nice blogs. It is helping me to learn boost, spirit module.
    Following test case for one double is failing.generator is appending 0 after decimal. Can it be prevented?

    str.clear();
    ls.kGenerate1(75., str);
    EXPECT_EQ("75.", str);

    [ RUN ] BoostSpiritAtoi.Simple
    \documents\visual studio 2012\projects\learnspirit\learnspirit\l
    earnspirittest.cpp(24): error: Value of: str
    Actual: "75.0"
    Expected: "75."
    [ FAILED ] BoostSpiritAtoi.Simple (1 ms)

    Thanks
    Nikhil

    ReplyDelete
    Replies
    1. Hello Nikhil, thank you for your kind words. I am sorry I have not used this fine library in a long while, and I can't answer your question right now.

      Delete
    2. On a second thought, I had an idea to solve your problem. I change the post to show it up. Thank you for making me thinking about it.

      Delete