Jak sformatować deble w następujący sposób?

Używam C++ i chciałbym sformatować deble w następujący oczywisty sposób. Próbowałem grać z „naprawionym” i „naukowym” przy użyciu stringstream, ale nie jestem w stanie osiągnąć pożądanego wyniku.

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"

Zgodnie z prośbą, oto rzeczy, które wypróbowałem:

#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;
}

To drukuje następujące informacje:

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 źródło
comment
Czy przeglądałeś tę stronę? Podaj kod, którego używasz i opisz, w jaki sposób nie spełnia on Twoich potrzeb.   -  person Jeff Hammond    schedule 31.08.2015
comment
To może być interesujące: github.com/cppformat/cppformat   -  person Galik    schedule 31.08.2015
comment
Myślę, choć nie jestem pewien, że Twoim celem jest najkrótsza możliwa reprezentacja dziesiętna, która po odczytaniu przez operator>> daje pierwotną wartość w pamięci. To pokazuje, że 3.14 i 3.140 są takie same — zawsze możesz usunąć końcowe zera. Ale to stwierdzenie wyjaśnia również, dlaczego można usunąć cyfry niezerowe.   -  person MSalters    schedule 31.08.2015


Odpowiedzi (4)


Program nie ma możliwości WIEDZIEĆ, jak sformatować liczby w sposób, który opisujesz, chyba że napiszesz kod, który w jakiś sposób przeanalizuje liczby - a nawet to może być dość trudne.

Wymagana jest tutaj znajomość formatu wejściowego w kodzie źródłowym, a ta zostaje utracona, gdy tylko kompilator konwertuje dziesiętny wejściowy kod źródłowy na postać binarną w celu zapisania go w pliku wykonywalnym.

Jedną z alternatywnych rozwiązań, która może zadziałać, jest wyprowadzenie wyniku do stringstream, a następnie zmodyfikowanie wyniku w celu usunięcia zer końcowych. Coś takiego:

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;
}

Właściwie to go nie męczyłem, ale jako przybliżony szkic powinien działać. Zastrzeżenia są takie, że jeśli masz coś w rodzaju 0,1, może to zostać wydrukowane jako 0,09999999999999285 lub coś w tym rodzaju, ponieważ 0,1 nie można przedstawić w dokładnej formie w postaci binarnej.

person Mats Petersson    schedule 31.08.2015
comment
Myślę, że coś takiego może być jedyną odpowiedzią (sformatuj przy użyciu standardowych funkcji C++, a następnie ręcznie usuń dopełnione zera). Być może uda mi się zoptymalizować, aby nie wymagać pętli while. - person CustomCalc; 31.08.2015

Dokładne formatowanie binarnych liczb zmiennoprzecinkowych jest dość trudne i tradycyjnie było błędne. W dwóch artykułach opublikowanych w 1990 roku w tym samym czasopiśmie ustalono, że wartości dziesiętne konwertowane na binarne liczby zmiennoprzecinkowe i odwrotnie mogą zostać przywrócone, pod warunkiem, że nie używają większej liczby cyfr dziesiętnych niż określone ograniczenie (w C++ reprezentowane przez std::numeric_limits<T>::digits10 dla odpowiedni typ T):

W czasie publikacji tych artykułów dyrektywy dotyczące formatowania C dla binarnych zmiennoprzecinkowych ("%f", "%e" i "%g") były dobrze ugruntowane i nie uległy zmianie, aby uwzględnić nowe wyniki. Problem ze specyfikacją tych dyrektyw formatujących polega na tym, że "%f" zakłada zliczanie cyfr po przecinku dziesiętnym i nie ma specyfikatora formatu proszącego o sformatowanie liczb z określoną liczbą cyfr, ale niekoniecznie rozpoczynających liczenie od przecinka dziesiętnego (np. formatować z kropką dziesiętną, ale potencjalnie mając wiele zer wiodących).

Specyfikatory formatu nadal nie są ulepszone, np. w celu uwzględnienia innego dla notacji nienaukowej, która może zawierać wiele zer. W efekcie moc algorytmu Steele/White'a nie jest w pełni ujawniona. Niestety formatowanie C++ nie poprawiło sytuacji i po prostu deleguje semantykę do dyrektyw formatowania C.

Podejście nie ustawiania std::ios_base::fixed i używania precyzji std::numeric_limits<double>::digits10 jest najbliższym przybliżeniem formatowania zmiennoprzecinkowego oferowanego przez standardowe biblioteki C i C++. Dokładny żądany format można uzyskać, uzyskując cyfry przy użyciu formatowania z std::ios_base::scientific, analizując wynik i później przepisując cyfry. Aby nadać temu procesowi ładny interfejs przypominający strumień, można enkapsulować go za pomocą aspektu std::num_put<char>.

Alternatywą może być zastosowanie podwójnej konwersji. Ta implementacja wykorzystuje ulepszony (szybszy) algorytm konwersji. Udostępnia także interfejsy umożliwiające uzyskanie cyfr w jakiejś formie, chociaż nie bezpośrednio jako sekwencja znaków, jeśli dobrze pamiętam.

person Dietmar Kühl    schedule 31.08.2015

Nie możesz zrobić tego, co chcesz, ponieważ liczb dziesiętnych nie można przedstawić dokładnie w formacie zmiennoprzecinkowym. Innymi słowy, double nie może dokładnie pomieścić 3.14, przechowuje wszystko jako ułamek potęgi 2, więc przechowuje to jako coś w rodzaju 3 + 9175/65536 lub mniej więcej (zrób to na swoim kalkulatorze, a otrzymasz 3,1399993896484375. ( Zdaję sobie sprawę, że 65536 nie jest właściwym mianownikiem dla podwójnego IEEE, ale jego istota jest poprawna).

Nazywa się to problemem podróży w obie strony. Nie można tego zrobić wiarygodnie

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

i uzyskaj „3,14”

Jeśli musisz rozwiązać problem w obie strony, nie używaj zmiennoprzecinkowego. Użyj niestandardowej klasy „dziesiętnej” lub użyj ciągu do przechowywania wartości.

Oto klasa dziesiętna, której możesz użyć: https://stackoverflow.com/a/15320495/364818

person Mark Lakata    schedule 31.08.2015
comment
Zmodyfikowałem moje pytanie, aby wyjaśnić, że błąd zmiennoprzecinkowy jest akceptowalny. Mój problem dotyczy formatowania, a nie dokładności. - person CustomCalc; 31.08.2015
comment
Można dokładnie odtworzyć wartości dziesiętne do std::numeric_limits<double>::digits10 (czyli 16). Ponieważ binarne wartości zmiennoprzecinkowe mogą być znormalizowane (tak jest, jeśli używany jest IEEE 754), końcowe zera ułamkowe mogą jednak nie zostać odzyskane. Nadal wartość się nie zmienia, ponieważ te zera nie mają wpływu na wartość. Odpowiednie artykuły to Jak dokładnie czytać liczby zmiennoprzecinkowe i Steele/White'a, jak dokładnie drukować liczby zmiennoprzecinkowe. - person Dietmar Kühl; 31.08.2015
comment
Pomijając kwestie pedantyczne: dokładny współczynnik IEEE najbliższy 3,14 to (1,0+2567051787601183,0/4503599627370496,0)*2. Pomijając kwestie pedantyczne: C++ 11 oferuje rozwiązanie problemu okrężnego, format hexfloat. Jest to taki sam format jak format printf %a, który jest częścią C od 1999. Działa to świetnie w przypadku punktów kontrolnych/restartu, serializacji/deserializacji i marshal/unmarshal. To w ogóle nie działa, jeśli dane wyjściowe mają być czytelne dla człowieka. - person David Hammen; 31.08.2015

Używam C++ i chciałbym sformatować deble w następujący oczywisty sposób.

Na podstawie twoich próbek zakładam, że chcesz

  • Naprawiono zamiast notacji naukowej,
  • Rozsądna (ale nie nadmierna) precyzja (dotyczy wyświetlania użytkownika, więc niewielkie zaokrąglenie jest w porządku),
  • Zera końcowe obcięte, i
  • Przecinek dziesiętny jest również obcinany, jeśli liczba wygląda jak liczba całkowita.

Poniższa funkcja właśnie to robi:

    #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;
    }

Jeśli używasz komputera, który wykorzystuje zmiennoprzecinkowy IEEE, zmiana prec_limit na 16 jest niewskazana. Chociaż pozwoli to poprawnie wydrukować 0,999999999999999 jako takie, drukuje również 5,1 jako 5,0999999999999996 i 9,99999998 jako 9,9999999800000001. To pochodzi z mojego komputera. Wyniki mogą się różnić ze względu na inną bibliotekę.

Zmiana prec_limit na 15 jest w porządku, ale nadal prowadzi do liczb, które nie są drukowane „poprawnie”. Podana wartość (14) działa dobrze, o ile nie próbujesz wydrukować 1.0-1e-15.

Mógłbyś zrobić jeszcze lepiej, ale może to wymagać odrzucenia standardowej biblioteki (patrz odpowiedź Dietmara Kühla).

person David Hammen    schedule 31.08.2015