对短字符串编码的有理数进行算术运算时,浮点数的简单替代方案

时间:2018-12-02 00:29:40

标签: c++ string floating-point rounding boost-multiprecision

我正在为将存储为字符串的“有理数”的数字取整的函数创建单元测试。当前的舍入实现将字符串强制转换为浮点类型:

#include <boost/lexical_cast.hpp>

#include <iomanip>
#include <limits>
#include <sstream>

template<typename T = double, 
         size_t PRECISION = std::numeric_limits<T>::digits10>
std::string Round(const std::string& number)
{
    std::stringstream ss{};
    ss << std::fixed << std::setprecision(PRECISION);
    ss << boost::lexical_cast<T>(number);
    return ss.str();
}

在我的一项测试中,我输入了数字3.55,该数字在我的机器上表示为3.5499999...。从2个小数点舍入到10个位时,一切都很好。但是,当我舍入到第一个小数点时,我毫不奇怪地得到3.5而不是3.6。

避免这种错误的简单方法是什么?

当前,我能够找到的最佳解决方案是使用多精度类型:

#include <boost/multiprecision/cpp_dec_float.hpp>

#include <iomanip>
#include <sstream>

template<size_t PRECISION = 10>
std::string Round(const std::string& number)
{
    using FixedPrecision = 
        boost::multiprecision::number<
            boost::multiprecision::cpp_dec_float<PRECISION>>;

    std::stringstream ss{};
    ss << std::fixed << std::setprecision(PRECISION);
    ss << FixedPrecision{number};
    return ss.str();
}

尽管该解决方案以直接的方式解决了该问题(相对于手动解析字符串或创建有理数类),但我发现对于这样一个简单的问题,它显得过分了。

为了找到解决此问题的方法,我偷看了一些计算器的实现。我查看了gnome-calculator的源代码,发现它使用GNU MPFR。然后,我查看了SpeedCrunch的实现,发现它重新使用了与bc相同的代码,后者使用了有理类型(分子,分母)。

我在俯视什么吗?

2 个答案:

答案 0 :(得分:0)

如果您尝试将字符串四舍五入到给定的小数位数(n小数),则可以直接在字符串“人为的方式”上执行此操作:首先检查字符串是否有小数点。如果有,请检查小数点后是否有n+1位。如果是这样,但小于5,则可以将字符串的开头细分为小数n。如果大于5,则必须转换字符串,基本上回溯,直到找到非9的数字“ d”,将其替换为“ d + 1”,然后将找到的所有9都设置为0。 n + 1个小数之前的数字是9(例如-999.99879),在顶部(如果有符号,则在符号后)附加1,并将找到的所有9都设置为零(-1000.00879)。有点乏味且效率低下,但简单易懂,遵循语法学校的直觉。

答案 1 :(得分:0)

您什么都不丢失。第一个实现中的问题是它四舍五入:首先是从字符串到浮点的转换,然后是第二次从浮点到字符串的转换。

使用类似boost的多精度数字类型,您可以精确地进行第一个转换(不舍入),这可能是解决问题的最优雅的方法。

如果要避免使用多精度类型,则必须找到其他方法来表示有理数,如注释中所述。您可以使用整数来执行此操作,但是结果比boost解决方案要长得多:

#include <cmath>
#include <cstdlib>
#include <iomanip>
#include <sstream>

std::string Round(const std::string &number, size_t new_places)
{
    /* split the string at the decimal point */
    auto dot = number.find('.');
    if (dot == std::string::npos)
        return number;

    auto whole_s = number.substr(0, dot);
    auto dec_s = number.substr(dot + 1);

    /* count the number of decimal places */
    auto old_places = dec_s.size();
    if(old_places <= new_places)
        return number;

    /* convert to integer form */
    auto whole = atoll(whole_s.c_str());
    auto dec = atoll(dec_s.c_str());
    auto sign = (whole < 0) ? -1 : 1;
    whole = abs(whole);

    /* combine into a single integer (123.4567 -> 1234567) */
    auto old_denom = (long long)pow(10.0, old_places);
    auto numerator = whole * old_denom + dec;

    /* remove low digits by division (1234567 -> 12346) */
    auto new_denom = (long long)pow(10.0, new_places);
    auto scale = old_denom / new_denom;
    numerator = (numerator + scale / 2) / scale;

    /* split at the decimal point again (12346 -> 123.46) */
    whole = sign * (numerator / new_denom);
    dec = numerator % new_denom;

    /* convert back to string form */
    std::ostringstream oss;
    oss << whole << '.' << std::setw(new_places) << std::setfill('0') << dec;
    return oss.str();
}