Как отформатировать двойники следующим образом?

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

double d = -5; // print "-5"
double d = 1000000000; // print "1000000000"
double d = 3.14; // print "3.14"
double d = 0.00000000001; // print "0.00000000001"
// Floating point error is acceptable:
double d = 10000000000000001; // print "10000000000000000"

В соответствии с просьбой, вот что я пробовал:

#include <iostream>
#include <string>
#include <sstream>
#include <iomanip>

using namespace std;

string obvious_format_attempt1( double d )
{
    stringstream ss;
    ss.precision(15);
    ss << d;
    return ss.str();
}

string obvious_format_attempt2( double d )
{
    stringstream ss;
    ss.precision(15);
    ss << fixed;
    ss << d;
    return ss.str();
}

int main(int argc, char *argv[]) 
{
    cout << "Attempt #1" << endl;
    cout << obvious_format_attempt1(-5) << endl;
    cout << obvious_format_attempt1(1000000000) << endl;
    cout << obvious_format_attempt1(3.14) << endl;
    cout << obvious_format_attempt1(0.00000000001) << endl;
    cout << obvious_format_attempt1(10000000000000001) << endl;

    cout << endl << "Attempt #2" << endl;
    cout << obvious_format_attempt2(-5) << endl;
    cout << obvious_format_attempt2(1000000000) << endl;
    cout << obvious_format_attempt2(3.14) << endl;
    cout << obvious_format_attempt2(0.00000000001) << endl;
    cout << obvious_format_attempt2(10000000000000001) << endl;

    return 0;
}

Это печатает следующее:

Attempt #1
-5
1000000000
3.14
1e-11
1e+16

Attempt #2
-5.000000000000000
1000000000.000000000000000
3.140000000000000
0.000000000010000
10000000000000000.000000000000000

person CustomCalc    schedule 30.08.2015    source источник
comment
Вы просматривали эту страницу? Укажите код, который вы используете, и опишите, почему он не соответствует вашим потребностям.   -  person Jeff Hammond    schedule 31.08.2015
comment
Это может быть интересно: github.com/cppformat/cppformat   -  person Galik    schedule 31.08.2015
comment
Я думаю, но я не уверен, что ваша цель состоит в том, чтобы иметь кратчайшее возможное десятичное представление, которое при обратном чтении operator>> дает вам исходное значение в памяти. Это показывает, что 3.14 и 3.140 одинаковы — вы всегда можете удалить конечные нули. Но это утверждение также объясняет, почему ненулевые цифры могут быть удалены.   -  person MSalters    schedule 31.08.2015


Ответы (4)


Программа не может ЗНАТЬ, как форматировать числа так, как вы описываете, если только вы не напишете код для анализа чисел каким-то образом, и даже это может быть довольно сложно.

Здесь требуется знание формата ввода в вашем исходном коде, и это теряется, как только компилятор преобразует десятичный исходный код ввода в двоичную форму для сохранения в исполняемом файле.

Одна альтернатива, которая может работать, состоит в том, чтобы выводить в stringstream, а затем изменять вывод, удаляя конечные нули. Что-то вроде этого:

string obvious_format_attempt2( double d )
{
    stringstream ss;
    ss.precision(15);
    ss << fixed;
    ss << d;
    string res = ss.str();
    // Do we have a dot?
    if ((string::size_type pos = res.rfind('.')) != string::npos)
    {
       while(pos > 0 && (res[pos] == '0' || res[pos] == '.')
       {
           pos--;
       }
       res = res.substr(pos);
    }
    return res;
}

На самом деле я не устал от этого, но как грубый набросок он должен работать. Предостережение заключается в том, что если у вас есть что-то вроде 0,1, оно может быть напечатано как 0,099999999999999285 или что-то в этом роде, потому что 0,1 не может быть представлено в точной форме как двоичный файл.

person Mats Petersson    schedule 31.08.2015
comment
Я думаю, что что-то вроде этого может быть единственным ответом (форматировать с использованием стандартных функций С++, а затем вручную стереть дополненные нули). Возможно, я могу оптимизировать, чтобы не требовать цикла while. - person CustomCalc; 31.08.2015

Точное форматирование двоичных чисел с плавающей запятой довольно сложно и традиционно ошибочно. Пара статей, опубликованных в 1990 году в том же журнале, установила, что десятичные значения, преобразованные в двоичные числа с плавающей запятой и обратно, могут быть восстановлены, если предположить, что они не используют больше десятичных цифр, чем определенное ограничение (в C++, представленном с использованием std::numeric_limits<T>::digits10 для соответствующий тип T):

На момент публикации этих документов директивы форматирования C для двоичных чисел с плавающей запятой ("%f", "%e" и "%g") были хорошо известны, и они не претерпели изменений, чтобы учесть новые результаты. Проблема со спецификацией этих директив форматирования заключается в том, что "%f" предполагает подсчет цифр после запятой, и нет спецификатора формата, запрашивающего форматирование чисел с определенным количеством цифр, но не обязательно начиная отсчет с десятичной запятой (например, для форматирования с десятичной точкой, но потенциально с большим количеством начальных нулей).

Спецификаторы формата до сих пор не улучшены, например, чтобы включить еще один для ненаучной записи, возможно, включающей много нулей, если на то пошло. По сути, мощь алгоритма Стила/Уайта раскрыта не полностью. Форматирование C++, к сожалению, не улучшило ситуацию и просто делегирует семантику директивам форматирования C.

Подход, при котором не устанавливается std::ios_base::fixed и используется точность std::numeric_limits<double>::digits10, является наиболее близким приближением к форматированию с плавающей запятой, которое предлагают стандартные библиотеки C и C++. Точный запрошенный формат может быть получен путем получения цифр с помощью форматирования с помощью std::ios_base::scientific, анализа результата и последующей перезаписи цифр. Чтобы придать этому процессу приятный потоковый интерфейс, его можно инкапсулировать с помощью фасета std::num_put<char>.

Альтернативой может быть использование двойного преобразования. В этой реализации используется улучшенный (более быстрый) алгоритм преобразования. Он также предоставляет интерфейсы для получения цифр в той или иной форме, но не напрямую в виде последовательности символов, если я правильно помню.

person Dietmar Kühl    schedule 31.08.2015

Вы не можете делать то, что хотите, потому что десятичные числа не могут быть точно представлены в формате с плавающей запятой. Другими словами, double не может точно содержать 3.14, он хранит все в виде долей степени двойки, поэтому он сохраняет это как что-то вроде 3 + 9175/65536 или около того (сделайте это на своем калькуляторе, и вы получите 3,1399993896484375. ( Я понимаю, что 65536 не является правильным знаменателем для IEEE double, но суть его верна).

Это известно как проблема кругового пути. Вы не можете надежно сделать

double x = 3.14;
cout << magic << x;

и получите "3.14"

Если вы должны решить проблему кругового пути, не используйте плавающую точку. Используйте пользовательский «десятичный» класс или используйте строку для хранения значения.

Вот десятичный класс, который вы могли бы использовать: https://stackoverflow.com/a/15320495/364818

person Mark Lakata    schedule 31.08.2015
comment
Я отредактировал свой вопрос, чтобы уточнить, что ошибка с плавающей запятой допустима. Моя проблема с форматированием, а не с точностью. - person CustomCalc; 31.08.2015
comment
Десятичные значения до std::numeric_limits<double>::digits10 (то есть 16) могут быть точно восстановлены. Однако, поскольку двоичные значения с плавающей запятой могут быть нормализованы (при использовании IEEE 754 они нормализованы), нули в конце дробной части не могут быть восстановлены. Тем не менее значение не меняется, так как эти нули не влияют на значение. Соответствующие документы: и статью Стила/Уайта Как правильно печатать числа с плавающей запятой. - person Dietmar Kühl; 31.08.2015
comment
Педантичное отступление: точный двойник IEEE, ближайший к 3.14, равен (1.0+2567051787601183.0/4503599627370496.0)*2. Непедантичный в сторону: C++11 предлагает решение проблемы циклического перебора, формат hexfloat. Это то же самое, что и формат printf %a, который был частью C с 1999 года. Он отлично работает для контрольной точки/перезапуска, сериализации/десериализации и маршалирования/демаршалирования. Это вообще не работает, если вывод предназначен для удобочитаемости. - person David Hammen; 31.08.2015

Я использую С++, и я хотел бы отформатировать двойные числа следующим очевидным образом.

Основываясь на ваших образцах, я предполагаю, что вы хотите

  • Фиксированная, а не научная запись,
  • Разумная (но не чрезмерная) точность (это для отображения пользователем, поэтому небольшое округление допустимо),
  • Нули в конце усекаются, и
  • Десятичная точка также усекается, если число выглядит как целое.

Следующая функция делает именно это:

    #include <cmath>
    #include <iomanip>
    #include <sstream>
    #include <string>


    std::string fixed_precision_string (double num) {

        // Magic numbers
        static const int prec_limit = 14;       // Change to 15 if you wish
        static const double log10_fuzz = 1e-15; // In case log10 is slightly off
        static const char decimal_pt = '.';     // Better: use std::locale

        if (num == 0.0) {
            return "0";
        }

        std::string result;

        if (num < 0.0) {
            result = '-';
            num = -num;
        }

        int ndigs = int(std::log10(num) + log10_fuzz);
        std::stringstream ss;
        if (ndigs >= prec_limit) {
            ss << std::fixed
               << std::setprecision(0)
               << num;
            result += ss.str();
        }
        else {
            ss << std::fixed
               << std::setprecision(prec_limit-ndigs)
               << num;
            result += ss.str();
            auto last_non_zero = result.find_last_not_of('0');
            if (result[last_non_zero] == decimal_pt) {
                result.erase(last_non_zero); 
            }
            else if (last_non_zero+1 < result.length()) {
                result.erase(last_non_zero+1);
            }
        }
        return result;
    }

Если вы используете компьютер, использующий IEEE с плавающей запятой, изменение prec_limit на 16 нежелательно. Хотя это позволит вам правильно напечатать 0,99999999999999999 как таковое, оно также напечатает 5.1 как 5,0999999999999996 и 9,99999998 как 9,9999999800000001. Это с моего компьютера, ваши результаты могут отличаться из-за другой библиотеки.

Изменение prec_limit на 15 допустимо, но это все равно приводит к тому, что числа печатаются «неправильно». Указанное значение (14) прекрасно работает, пока вы не пытаетесь напечатать 1.0-1e-15.

Вы могли бы сделать еще лучше, но для этого может потребоваться отказ от стандартной библиотеки (см. Ответ Дитмара Кюля).

person David Hammen    schedule 31.08.2015