【问题标题】:Parsing fixed width numbers with boost spirit用提升精神解析固定宽度的数字
【发布时间】:2021-10-04 21:07:19
【问题描述】:

我正在使用 Spirit 解析填充了固定宽度数字的类似 fortran 的文本文件:

1234 0.000000000000D+001234
1234 7.654321000000D+001234
1234                   1234
1234-7.654321000000D+001234

有符号和无符号整数的解析器,但我找不到固定宽度实数的解析器,有人可以帮忙吗?

这就是我所拥有的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";
        }
    }
}

这显然不能解析大多数记录:

Parse fail ("1234 0.000000000000D+001234")
Parse fail ("1234 7.654321000000D+001234")
{a:1234, b:0, c:1234}
Parse fail ("1234-7.654321000000D+001234")

【问题讨论】:

    标签: c++ parsing boost-spirit boost-spirit-qi floating-point-conversion


    【解决方案1】:

    该机制存在,但隐藏得更深,因为解析浮点数的细节比整数多得多。

    qi::double_(和float_)实际上是qi::real_parser&lt;double, qi::real_policies&lt;double&gt; &gt; 的实例。

    policies 是关键。它们管理所接受格式的所有细节。

    这里是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.

    让我们实施您的政策:

    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
    

    注意:

    • 我保持属性类型通用。
    • 我也基于严格的实现 strict_urealpolicies 以减少工作量。基类没有 支持符号,并且需要一个强制性的小数分隔符 ('.'),这使得它“严格”并且只拒绝整数
    • 您的问题格式要求整数部分为 1 位,整数部分为 12 位 指数的分数和 2,但我没有硬编码,所以我们可以重用 其他固定宽度格式的政策(IDigitsFDigitsEDigits

    让我们一个一个地检查我们的覆盖:

    bool parse_sign(f, l)

    格式是定宽的,所以想接受

    • 前导空格或'+' 表示肯定
    • 前导“-”表示否定

    这样标志总是需要一个输入字符:

    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&amp; a)

    最简单的部分:我们只允许在分隔符之前有一位数 (IDigits) 无符号整数部分。幸运的是,整数解析是比较常见和简单的:

    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)

    也很简单:我们总是需要'D'

    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&amp; a)

    至于指数,我们希望它是固定宽度的,这意味着符号是 强制的。因此,在提取宽度为 2 的有符号整数 (EDigits) 之前,我们确保 标志存在:

    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&amp;, int&amp; a)

    问题的实质,也是建立在现有解析器上的原因。 小数位可以被认为是整数,但是由于以下原因存在问题 前导零很重要以及总位数可能 超过我们选择的任何整数类型的容量。

    所以我们做了一个“技巧”——我们解析一个无符号整数,但忽略任何多余的 不适合的精度:实际上我们只关心位数。我们 然后检查这个数字是否符合预期:FDigits

    然后,我们交给基类实现来实际计算 对于任何通用数字类型T(满足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;
    }
    

    总结

    您可以看到,站在现有的、经过测试的代码的肩膀上,我们已经完成并且可以很好地解析我们的数字:

    template <typename T>
    using X19_type = qi::real_parser<T, policies::fixed_widths_D<T, 1, 12, 2>>;
    

    现在您的代码按预期运行: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";
            }
        }
    }
    

    打印

    {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}
    

    小数

    现在,可以以超过 double 的精度。从 十进制数字到不精确的二进制表示。展示如何选择 对于通用的T 已经满足了这一点,让我们用十进制类型进行实例化 允许 64 位有效的十进制小数位:

    Live On Coliru

    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";
            }
        }
    }
    

    打印

    {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}
    

    比较如何使用二进制long double 表示将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}
    

    奖金:可选

    在当前 RECORD 中,缺失的双打被默默地视为0.0。这可能不是最好的:

    struct RECORD {
        uint16_t          a{};
        optional<Decimal> b{};
        uint16_t          c{};
    };
    
    // ...
    
    qi::rule<It, optional<Decimal>()> X71 = x71 //
        | qi::repeat(71)[' '];
    

    现在输出是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}
    

    总结/添加单元测试!

    这很多,但可能不是您需要的全部。

    请记住,您仍然需要适当的单元测试,例如X19_type。思考 在您可能遇到/想要接受/想要拒绝的所有边缘情况中:

    • 我没有更改任何处理 Inf 或 NaN 的基本策略,所以你 可能想缩小这些差距
    • 你可能真的想接受" 3.141 "" .999999999999D+0 " 等等?

    所有这些都是对策略的非常简单的更改,但是,如您所知,代码 没有测试就坏了。


    【讨论】:

    • 除了上面的注释之外,我们在解析定宽值的时候,通常不仅会检查小数部分的宽度,还会检查字段的整个宽度,所以我添加了这个定义来做它。
    • 我认为如果所有部分都需要固定宽度并且始终存在,那么额外的检查应该是多余的。
    • 也许我忘了检查“缺少指数”的情况?你可以做的最简单的是qi::raw [ x18 [ _val = _1] ] [ _pass = (19 == boost::phoenix::stl::size(_1)) ]
    • 代码快照对于注释来说太大了。一般来说,这个数字不应该被标准化,所以以下表示对于 D14.5 是合法的:1.12345D+02 112.34500D+00 112.34500
    • 什么是 D14.5?请注意,除了“长度 19”和“+0.000000000000D+00”之外,您没有指定任何要求
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多