I'm using spirit to parse fortran-like text file filled with fixed width numbers:
1234 0.000000000000D+001234
1234 7.654321000000D+001234
1234 1234
1234-7.654321000000D+001234
There are parsers for signed and unsigned integers, but I can not find a parser for fixed width real numbers, can someone help with it ?
Here's what I have Live On Coliru
#include <boost/spirit/include/qi.hpp>
#include <boost/fusion/adapted.hpp>
#include <iomanip>
namespace qi = boost::spirit::qi;
struct RECORD {
uint16_t a{};
double b{};
uint16_t c{};
};
BOOST_FUSION_ADAPT_STRUCT(RECORD, a,b,c)
int main() {
using It = std::string::const_iterator;
using namespace qi::labels;
qi::uint_parser<uint16_t, 10, 4, 4> i4;
qi::rule<It, double()> X19 = qi::double_ //
| qi::repeat(19)[' '] >> qi::attr(0.0);
for (std::string const str : {
"1234 0.000000000000D+001234",
"1234 7.654321000000D+001234",
"1234 1234",
"1234-7.654321000000D+001234",
}) {
It f = str.cbegin(), l = str.cend();
RECORD rec;
if (qi::parse(f, l, (i4 >> X19 >> i4), rec)) {
std::cout << "{a:" << rec.a << ", b:" << rec.b << ", c:" << rec.c
<< "}\n";
} else {
std::cout << "Parse fail (" << std::quoted(str) << ")\n";
}
}
}
Which obviously doesn't parse most records:
Parse fail ("1234 0.000000000000D+001234")
Parse fail ("1234 7.654321000000D+001234")
{a:1234, b:0, c:1234}
Parse fail ("1234-7.654321000000D+001234")
The mechanism exists, but it's hidden more deeply because there are many more details to parsing floating point numbers than integers.
qi::double_
(and float_
) are actually instances of qi::real_parser<double, qi::real_policies<double> >
.
The policies are the key. They govern all the details of what format is accepted.
Here are the RealPolicies Expression Requirements
Expression | Semantics |
---|---|
RP::allow_leading_dot |
Allow leading dot. |
RP::allow_trailing_dot |
Allow trailing dot. |
RP::expect_dot |
Require a dot. |
RP::parse_sign(f, l) |
Parse the prefix sign (e.g. '-'). Return true if successful, otherwise false . |
RP::parse_n(f, l, n) |
Parse the integer at the left of the decimal point. Return true if successful, otherwise false . If successful, place the result into n. |
RP::parse_dot(f, l) |
Parse the decimal point. Return true if successful, otherwise false . |
RP::parse_frac_n(f, l, n, d) |
Parse the fraction after the decimal point. Return true if successful, otherwise false . If successful, place the result into n and the number of digits into d |
RP::parse_exp(f, l) |
Parse the exponent prefix (e.g. 'e'). Return true if successful, otherwise false . |
RP::parse_exp_n(f, l, n) |
Parse the actual exponent. Return true if successful, otherwise false . If successful, place the result into n. |
RP::parse_nan(f, l, n) |
Parse a NaN. Return true if successful, otherwise false . If successful, place the result into n. |
RP::parse_inf(f, l, n) |
Parse an Inf. Return true if successful, otherwise false . If successful, place the result into n. |
Let's implement your policies:
namespace policies {
/* mandatory sign (or space) fixed widths, 'D+' or 'D-' exponent leader */
template <typename T, int IDigits, int FDigits, int EDigits = 2>
struct fixed_widths_D : qi::strict_ureal_policies<T> {
template <typename It> static bool parse_sign(It& f, It const& l);
template <typename It, typename Attr>
static bool parse_n(It& f, It const& l, Attr& a);
template <typename It> static bool parse_exp(It& f, It const& l);
template <typename It>
static bool parse_exp_n(It& f, It const& l, int& a);
template <typename It, typename Attr>
static bool parse_frac_n(It& f, It const& l, Attr& a, int& n);
};
} // namespace policies
Note:
strict_urealpolicies
to reduce the effort. The base class doesn't
support signs, and requires a mandatory decimal separator ('.'
), which makes it "strict" and rejecting just integral numbersIDigits
, FDigits
, EDigits
)Let's go through our overrides one-by-one:
bool parse_sign(f, l)
The format is fixed-width, so want to accept
'+'
for positiveThat way the sign always takes one input character:
template <typename It> static bool parse_sign(It& f, It const&l)
{
if (f != l) {
switch (*f) {
case '+':
case ' ': ++f; break;
case '-': ++f; return true;
}
}
return false;
}
bool parse_n(f, l, Attr& a)
The simplest part: we allow only a single-digit (IDigits
) unsigned integer part before the separator. Luckily, integer parsing is relatively common and trivial:
template <typename It, typename Attr>
static bool parse_n(It& f, It const& l, Attr& a)
{
return qi::extract_uint<Attr, 10, IDigits, IDigits, false, true>::call(f, l, a);
}
bool parse_exp(f, l)
Also trivial: we require a 'D'
always:
template <typename It> static bool parse_exp(It& f, It const& l)
{
if (f == l || *f != 'D')
return false;
++f;
return true;
}
bool parse_exp_n(f, l, int& a)
As for the exponent, we want it to be fixed-width meaning that the sign is
mandatory. So, before extracting the signed integer of width 2 (EDigits
), we make sure a
sign is present:
template <typename It>
static bool parse_exp_n(It& f, It const& l, int& a)
{
if (f == l || !(*f == '+' || *f == '-'))
return false;
return qi::extract_int<int, 10, EDigits, EDigits>::call(f, l, a);
}
bool parse_frac_n(f, l, Attr&, int& a)
The meat of the problem, and also the reason to build on the existing parsers. The fractional digits could be considered integral, but there are issues due to leading zeroes being significant as well as the total number of digits might exceed the capacity of any integral type we choose.
So we do a "trick" - we parse an unsigned integer, but ignoring any excess
precision that doesn't fit: in fact we only care about the number of digits. We
then check that this number is as expected: FDigits
.
Then, we hand off to the base class implementation to actually compute the
resulting value correctly, for any generic number type T
(that satisfies the
minimum
requirements).
template <typename It, typename Attr>
static bool parse_frac_n(It& f, It const& l, Attr& a, int& n)
{
It savef = f;
if (qi::extract_uint<Attr, 10, FDigits, FDigits, true, true>::call(f, l, a)) {
n = static_cast<int>(std::distance(savef, f));
return n == FDigits;
}
return false;
}
You can see, by standing on the shoulders of existing, tested code we're already done and good to parse our numbers:
template <typename T>
using X19_type = qi::real_parser<T, policies::fixed_widths_D<T, 1, 12, 2>>;
Now your code runs as expected: Live On Coliru
template <typename T>
using X19_type = qi::real_parser<T, policies::fixed_widths_D<T, 1, 12, 2>>;
int main() {
using It = std::string::const_iterator;
using namespace qi::labels;
qi::uint_parser<uint16_t, 10, 4, 4> i4;
X19_type<double> x19;
qi::rule<It, double()> X19 = x19 //
| qi::repeat(19)[' '] >> qi::attr(0.0);
for (std::string const str : {
"1234 1234",
"1234 0.000000000000D+001234",
"1234 7.065432100000D+001234",
"1234-7.006543210000D+001234",
"1234 0.065432100000D+031234",
"1234 0.065432100000D-301234",
}) {
It f = str.cbegin(), l = str.cend();
RECORD rec;
if (qi::parse(f, l, (i4 >> X19 >> i4), rec)) {
std::cout << "{a:" << rec.a << ", b:" << std::setprecision(12)
<< rec.b << ", c:" << rec.c << "}\n";
} else {
std::cout << "Parse fail (" << std::quoted(str) << ")\n";
}
}
}
Prints
{a:1234, b:0, c:1234}
{a:1234, b:0, c:1234}
{a:1234, b:7.0654321, c:1234}
{a:1234, b:-7.00654321, c:1234}
{a:1234, b:65.4321, c:1234}
{a:1234, b:6.54321e-32, c:1234}
Now, it's possible to instantiate this parser with precisions that exceed the
precision of double
. And there are always issues with the conversion from
decimal numbers to inexact binary representation. To showcase how the choice
for generic T
already caters for this, let's instantiate with a decimal type
that allows 64 significant decimal fractional digits:
using Decimal = boost::multiprecision::cpp_dec_float_100;
struct RECORD {
uint16_t a{};
Decimal b{};
uint16_t c{};
};
template <typename T>
using X71_type = qi::real_parser<T, policies::fixed_widths_D<T, 1, 64, 2>>;
int main() {
using It = std::string::const_iterator;
using namespace qi::labels;
qi::uint_parser<uint16_t, 10, 4, 4> i4;
X71_type<Decimal> x71;
qi::rule<It, Decimal()> X71 = x71 //
| qi::repeat(71)[' '] >> qi::attr(0.0);
for (std::string const str : {
"1234 6789",
"2345 0.0000000000000000000000000000000000000000000000000000000000000000D+006789",
"3456 7.0000000000000000000000000000000000000000000000000000000000654321D+006789",
"4567-7.0000000000000000000000000000000000000000000000000000000000654321D+006789",
"5678 0.0000000000000000000000000000000000000000000000000000000000654321D+036789",
"6789 0.0000000000000000000000000000000000000000000000000000000000654321D-306789",
}) {
It f = str.cbegin(), l = str.cend();
RECORD rec;
if (qi::parse(f, l, (i4 >> X71 >> i4), rec)) {
std::cout << "{a:" << rec.a << ", b:" << std::setprecision(65)
<< rec.b << ", c:" << rec.c << "}\n";
} else {
std::cout << "Parse fail (" << std::quoted(str) << ")\n";
}
}
}
Prints
{a:2345, b:0, c:6789}
{a:3456, b:7.0000000000000000000000000000000000000000000000000000000000654321, c:6789}
{a:4567, b:-7.0000000000000000000000000000000000000000000000000000000000654321, c:6789}
{a:5678, b:6.54321e-56, c:6789}
{a:6789, b:6.54321e-89, c:6789}
Compare how using a binary
long double
representation would have lost accuracy here:{a:2345, b:0, c:6789} {a:3456, b:7, c:6789} {a:4567, b:-7, c:6789} {a:5678, b:6.5432100000000000002913506043764438647482181234694313277925965188e-56, c:6789} {a:6789, b:6.5432100000000000000601529073044049029207066886931600941449474131e-89, c:6789}
In the current RECORD, missing doubles are silently taken to be 0.0
. That's maybe not the best:
struct RECORD {
uint16_t a{};
optional<Decimal> b{};
uint16_t c{};
};
// ...
qi::rule<It, optional<Decimal>()> X71 = x71 //
| qi::repeat(71)[' '];
Now the output is Live On Coliru:
{a:1234, b:--, c:6789}
{a:2345, b: 0, c:6789}
{a:3456, b: 7.0000000000000000000000000000000000000000000000000000000000654321, c:6789}
{a:4567, b: -7.0000000000000000000000000000000000000000000000000000000000654321, c:6789}
{a:5678, b: 6.54321e-56, c:6789}
{a:6789, b: 6.54321e-89, c:6789}
That's a lot, but possibly not all you need.
Keep in mind that you still need proper unit tests for e.g. X19_type
. Think
of all edge cases you may encounter/want to accept/want to reject:
" 3.141 "
,
" .999999999999D+0 "
etc.?All these are pretty simple changes to the policies, but, as you know, code without tests is broken.