我正在为将存储为字符串的“有理数”的数字取整的函数创建单元测试。当前的舍入实现将字符串强制转换为浮点类型:
#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相同的代码,后者使用了有理类型(分子,分母)。
我在俯视什么吗?
答案 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();
}