Простые альтернативы с плавающей точкой при выполнении арифметики на рациональных кодировках с короткой строкой
Я создаю модульные тесты для функции, которая округляет "рациональные" числа, хранящиеся в виде строк. Текущая реализация округления приводит строки к типу с плавающей запятой:
#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();
}
Хотя это решение решает проблему простым способом (по сравнению с ручным синтаксическим анализом строк или созданием класса чисел Rational), я считаю его излишним для такой простой проблемы.
Чтобы найти способы решения этой проблемы, я заглянул в некоторые реализации калькуляторов. Я посмотрел на исходный код gnome-calculator и обнаружил, что он использует GNU MPFR. Затем я посмотрел на реализацию SpeedCrunch и обнаружил, что он повторно использует тот же код, что и bc, который использует рациональный тип (числитель, знаменатель).
Я что-то пропускаю?
0 ответов
Вы ничего не пропускаете. Проблема в вашей первой реализации заключается в том, что она округляется дважды: сначала при преобразовании из строки в число с плавающей запятой, а затем во второй раз при преобразовании строки обратно в строку.
Использование числового типа с высокой точностью, например, типа 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();
}
Если вы пытаетесь округлить строки для заданного числа десятичных знаков (n
десятичный), вы можете сделать это непосредственно в строке "по-человечески": сначала убедитесь, что строка имеет десятичную точку. если он есть, проверьте, есть ли у него n+1
цифра после десятичной точки. Если это так, но это меньше пяти, вы можете подстроковать заголовок строки до n
десятичный. Если оно больше пяти, вы должны преобразовать свою строку, в основном, возвращая обратно, пока не найдете не '9' цифру 'd', замените ее на 'd+1' и установите все найденные девятки равными 0. Если ВСЕ цифры перед десятичной дробью n+1 - это девятки (скажем, -999,9989), добавьте 1 сверху (после знака, если он есть) и установите все найденные девятки на ноль (-1000.00879). Немного утомительно и несколько неэффективно, но прямолинейно и следует интуиции гимназии.