Простые альтернативы с плавающей точкой при выполнении арифметики на рациональных кодировках с короткой строкой - PullRequest
0 голосов
/ 02 декабря 2018

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

#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, который использует рациональный тип (числитель, знаменатель).

Я что-то пропускаю?

Ответы [ 2 ]

0 голосов
/ 22 февраля 2019

Ты ничего не пропустил.Проблема в вашей первой реализации заключается в том, что она округляется дважды: сначала при преобразовании из строки в число с плавающей точкой, а затем во второй раз при преобразовании из строки с плавающей точкой в ​​строку.

Использование числового типа с высокой точностью, такого как 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();
}
0 голосов
/ 21 февраля 2019

Если вы пытаетесь округлить строки для заданного числа десятичных разрядов (n десятичное число), вы можете сделать это непосредственно со строкой «по-человечески»: сначала убедитесь, что строка имеет десятичную точку.если он есть, проверьте, есть ли у него цифра n+1 после десятичной точки.Если это так, но это меньше пяти, вы можете подстроковать заголовок строки до десятичного числа n.Если оно больше пяти, вы должны преобразовать свою строку, в основном, возвращая обратно, пока не найдете не '9' цифру 'd', замените ее на 'd + 1' и установите все найденные девятки равными 0. Если ВСЕцифры перед десятичной дробью n + 1 - это девятки (скажем, -999,9989), добавьте 1 сверху (после знака, если он есть) и установите все найденные девятки на ноль (-1000.00879).Немного утомительно и несколько неэффективно, но прямолинейно и следует интуиции гимназии.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...