Простые альтернативы с плавающей точкой при выполнении арифметики на рациональных кодировках с короткой строкой

Я создаю модульные тесты для функции, которая округляет "рациональные" числа, хранящиеся в виде строк. Текущая реализация округления приводит строки к типу с плавающей запятой:

#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). Немного утомительно и несколько неэффективно, но прямолинейно и следует интуиции гимназии.

Другие вопросы по тегам