2

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

#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 相同的代码,它采用了有理类型(分子、分母)。

我忽略了什么吗?

4

2 回答 2

0

你没有错过任何东西。您的第一个实现中的问题是它进行了两次舍入:第一次是从字符串到浮点数的转换,然后是第二次从浮点数到字符串的转换。

使用像 boost's 这样的多精度数字类型可以让您准确地进行第一次转换(不四舍五入),这可能是解决问题的最优雅的方法。

如果您想避免使用多精度类型,那么您必须找到其他方式来表示有理数,正如评论中已经说过的那样。您可以使用整数执行此操作,但结果比 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();
}
于 2019-02-21T21:47:13.167 回答
0

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

于 2019-02-21T20:13:41.443 回答