Unikaj końcowych zer w printf ()


107

Ciągle napotykam specyfikatory formatu dla rodziny funkcji printf (). Chcę móc wydrukować podwójną (lub zmiennoprzecinkową) z maksymalną podaną liczbą cyfr po przecinku. Jeśli używam:

printf("%1.3f", 359.01335);
printf("%1.3f", 359.00999);

dostaję

359.013
359.010

Zamiast pożądanego

359.013
359.01

Czy ktoś może mi pomóc?


1
Niedokładność zmiennoprzecinkowa naprawdę oznacza, że ​​powinieneś sam zaokrąglić. Weź wariant odpowiedzi R i Juha (który nie do końca obsługuje końcowe zera) i napraw go.
mądry

Odpowiedzi:


88

Nie można tego zrobić za pomocą normalnych printfspecyfikatorów formatu. Najbliższe możesz uzyskać:

printf("%.6g", 359.013); // 359.013
printf("%.6g", 359.01);  // 359.01

ale „.6” to całkowita szerokość liczbowa, więc

printf("%.6g", 3.01357); // 3.01357

niszczy to.

To, co możesz zrobić, to zmienićsprintf("%.20g") liczbę w buforze ciągów, a następnie manipulować łańcuchem tak, aby zawierał tylko N znaków po przecinku.

Zakładając, że twoja liczba znajduje się w zmiennej num, poniższa funkcja usunie wszystkie Ncyfry z wyjątkiem pierwszych miejsc po przecinku, a następnie usunie końcowe zera (i kropkę dziesiętną, jeśli wszystkie były zerami).

char str[50];
sprintf (str,"%.20g",num);  // Make the number.
morphNumericString (str, 3);
:    :
void morphNumericString (char *s, int n) {
    char *p;
    int count;

    p = strchr (s,'.');         // Find decimal point, if any.
    if (p != NULL) {
        count = n;              // Adjust for more or less decimals.
        while (count >= 0) {    // Maximum decimals allowed.
             count--;
             if (*p == '\0')    // If there's less than desired.
                 break;
             p++;               // Next character.
        }

        *p-- = '\0';            // Truncate string.
        while (*p == '0')       // Remove trailing zeros.
            *p-- = '\0';

        if (*p == '.') {        // If all decimals were zeros, remove ".".
            *p = '\0';
        }
    }
}

Jeśli nie jesteś zadowolony z aspektu obcinania (który zamieniłby się 0.12399w 0.123zamiast zaokrąglać go 0.124), możesz faktycznie skorzystać z funkcji zaokrąglania już udostępnionych przez printf. Wystarczy wcześniej przeanalizować liczbę, aby dynamicznie utworzyć szerokości, a następnie użyć ich do przekształcenia liczby w ciąg:

#include <stdio.h>

void nDecimals (char *s, double d, int n) {
    int sz; double d2;

    // Allow for negative.

    d2 = (d >= 0) ? d : -d;
    sz = (d >= 0) ? 0 : 1;

    // Add one for each whole digit (0.xx special case).

    if (d2 < 1) sz++;
    while (d2 >= 1) { d2 /= 10.0; sz++; }

    // Adjust for decimal point and fractionals.

    sz += 1 + n;

    // Create format string then use it.

    sprintf (s, "%*.*f", sz, n, d);
}

int main (void) {
    char str[50];
    double num[] = { 40, 359.01335, -359.00999,
        359.01, 3.01357, 0.111111111, 1.1223344 };
    for (int i = 0; i < sizeof(num)/sizeof(*num); i++) {
        nDecimals (str, num[i], 3);
        printf ("%30.20f -> %s\n", num[i], str);
    }
    return 0;
}

W nDecimals()tym przypadku chodzi o poprawne obliczenie szerokości pól, a następnie sformatowanie liczby przy użyciu ciągu formatu na tej podstawie. Uprząż testowa main()pokazuje to w akcji:

  40.00000000000000000000 -> 40.000
 359.01335000000000263753 -> 359.013
-359.00999000000001615263 -> -359.010
 359.00999999999999090505 -> 359.010
   3.01357000000000008200 -> 3.014
   0.11111111099999999852 -> 0.111
   1.12233439999999995429 -> 1.122

Po uzyskaniu prawidłowo zaokrąglonej wartości możesz ponownie przekazać ją, morphNumericString()aby usunąć końcowe zera, po prostu zmieniając:

nDecimals (str, num[i], 3);

w:

nDecimals (str, num[i], 3);
morphNumericString (str, 3);

(lub wywołanie morphNumericStringna końcu, nDecimalsale w takim przypadku prawdopodobnie po prostu połączyłbym te dwie funkcje w jedną funkcję), a otrzymasz:

  40.00000000000000000000 -> 40
 359.01335000000000263753 -> 359.013
-359.00999000000001615263 -> -359.01
 359.00999999999999090505 -> 359.01
   3.01357000000000008200 -> 3.014
   0.11111111099999999852 -> 0.111
   1.12233439999999995429 -> 1.122

8
Zauważ, że ta odpowiedź zakłada, że ​​miejsce dziesiętne to .W niektórych ustawieniach miejsce dziesiętne jest w rzeczywistości ,przecinkiem.

1
Właściwie jest tu drobna literówka - p = strchr (str, '.'); właściwie powinno być p = strchr (s, '.'); Aby użyć parametru funkcji zamiast globalnego var.
n3wtz

Użycie zbyt wielu cyfr może wywołać nieoczekiwane wyniki ... na przykład „% .20g” z wartością 0.1 spowoduje wyświetlenie śmieci. To, co musisz zrobić, jeśli nie chcesz zaskakiwać użytkowników, to zacząć od powiedzmy 15 cyfr i zwiększać wartość, aż uzyskasz atoftę samą wartość.
6502

@ 6502, to dlatego, że w IEEE754 nie ma czegoś takiego jak 0.1 :-) Jednak nie spowoduje to problemu (przynajmniej dla tej wartości), ponieważ wynik 0.10000000000000000555będzie 0.1po usunięciu . Problem może pojawić się, jeśli masz wartość nieco niższą, na przykład jeśli najbliższa reprezentacja 42.1była 42.099999999314159. Jeśli naprawdę chcesz sobie z tym poradzić, prawdopodobnie będziesz musiał zaokrąglić na podstawie ostatniej usuniętej cyfry, a nie skrócić.
paxdiablo

1
@paxdiablo: Nie ma potrzeby dynamicznego tworzenia ciągu formatującego dla printffunkcji-podobnych, ponieważ obsługują one już parametry dynamiczne ( *znak specjalny): na przykład printf("%*.*f", total, decimals, x);wyprowadza liczbę z dynamicznie określoną całkowitą długością pola i miejscami dziesiętnymi.
6502

54

Aby pozbyć się końcowych zer, należy użyć formatu „% g”:

float num = 1.33;
printf("%g", num); //output: 1.33

Po pewnym wyjaśnieniu pytania, że ​​pomijanie zer nie jest jedyną rzeczą, o którą pytano, ale wymagane było również ograniczenie wyjścia do trzech miejsc po przecinku. Myślę, że nie można tego zrobić za pomocą samych ciągów formatu sprintf. Jak zauważył Pax Diablo , manipulacja strunami byłaby wymagana.



@Tomalak: To nie robi tego, co chcę. Chcę mieć możliwość określenia maksymalnej liczby cyfr po przecinku. To jest: Chcę wydrukować 1.3347 "1.335" i 1.3397 "1.34" @xtofl: Już to sprawdziłem, ale nadal nie widzę odpowiedzi na mój problem
Gorpik

3
Autor chce stylu „f”, a nie stylu „e”. „g” może używać stylu „e”: Z dokumentacji: Styl e jest używany, jeśli wykładnik z jego konwersji jest mniejszy niż -4 lub większy lub równy precyzji.
Julien Palard

20

Podoba mi się odpowiedź R. nieco poprawiona:

float f = 1234.56789;
printf("%d.%.0f", f, 1000*(f-(int)f));

„1000” określa precyzję.

Moc do zaokrąglenia 0,5.

EDYTOWAĆ

Ok, ta odpowiedź była edytowana kilka razy i zgubiłem to, o czym myślałem kilka lat temu (i pierwotnie nie spełniała wszystkich kryteriów). Oto nowa wersja (która spełnia wszystkie kryteria i poprawnie obsługuje liczby ujemne):

double f = 1234.05678900;
char s[100]; 
int decimals = 10;

sprintf(s,"%.*g", decimals, ((int)(pow(10, decimals)*(fabs(f) - abs((int)f)) +0.5))/pow(10,decimals));
printf("10 decimals: %d%s\n", (int)f, s+1);

I przypadki testowe:

#import <stdio.h>
#import <stdlib.h>
#import <math.h>

int main(void){

    double f = 1234.05678900;
    char s[100];
    int decimals;

    decimals = 10;
    sprintf(s,"%.*g", decimals, ((int)(pow(10, decimals)*(fabs(f) - abs((int)f)) +0.5))/pow(10,decimals));
    printf("10 decimals: %d%s\n", (int)f, s+1);

    decimals = 3;
    sprintf(s,"%.*g", decimals, ((int)(pow(10, decimals)*(fabs(f) - abs((int)f)) +0.5))/pow(10,decimals));
    printf(" 3 decimals: %d%s\n", (int)f, s+1);

    f = -f;
    decimals = 10;
    sprintf(s,"%.*g", decimals, ((int)(pow(10, decimals)*(fabs(f) - abs((int)f)) +0.5))/pow(10,decimals));
    printf(" negative 10: %d%s\n", (int)f, s+1);

    decimals = 3;
    sprintf(s,"%.*g", decimals, ((int)(pow(10, decimals)*(fabs(f) - abs((int)f)) +0.5))/pow(10,decimals));
    printf(" negative  3: %d%s\n", (int)f, s+1);

    decimals = 2;
    f = 1.012;
    sprintf(s,"%.*g", decimals, ((int)(pow(10, decimals)*(fabs(f) - abs((int)f)) +0.5))/pow(10,decimals));
    printf(" additional : %d%s\n", (int)f, s+1);

    return 0;
}

A wynik testów:

 10 decimals: 1234.056789
  3 decimals: 1234.057
 negative 10: -1234.056789
 negative  3: -1234.057
 additional : 1.01

Teraz wszystkie kryteria są spełnione:

  • maksymalna liczba miejsc po przecinku za zerem jest ustalona
  • końcowe zera są usuwane
  • czy to matematycznie poprawne (prawda?)
  • działa (teraz) także wtedy, gdy pierwsza cyfra dziesiętna wynosi zero

Niestety ta odpowiedź jest dwuwierszowa, ponieważ sprintfnie zwraca ciągu.


1
Jednak to faktycznie nie wydaje się przycinać końcowych zer? Z przyjemnością wypluwa około 1,1000.
Dean J

Pytanie i moja odpowiedź nie są już oryginałami ... Sprawdzę później.
Juha

1
Przypadki testowe zakończone niepowodzeniem: 1.012 z programem formatującym "% .2g" da wynik 1.012 zamiast 1.01.
wtl

@wtl Niezły chwyt. Teraz naprawione.
Juha

@Jeroen: Myślę, że zmiennoprzecinkowe zawodzą również w pewnym momencie, gdy liczby stają się większe ... ale zasadniczo powinno działać rzutowanie na long, long long lub int64.
Juha

2

Wyszukuję w ciągu (zaczynając od prawej strony) pierwszy znak w zakresie 1do 9(wartość ASCII 49- 57), a następnie null(ustawiany na 0) każdy znak na prawo od niego - patrz poniżej:

void stripTrailingZeros(void) { 
    //This finds the index of the rightmost ASCII char[1-9] in array
    //All elements to the left of this are nulled (=0)
    int i = 20;
    unsigned char char1 = 0; //initialised to ensure entry to condition below

    while ((char1 > 57) || (char1 < 49)) {
        i--;
        char1 = sprintfBuffer[i];
    }

    //null chars left of i
    for (int j = i; j < 20; j++) {
        sprintfBuffer[i] = 0;
    }
}

2

A co z czymś takim (może zawierać błędy zaokrąglania i problemy z wartością ujemną, które wymagają debugowania, pozostawione jako ćwiczenie dla czytelnika):

printf("%.0d%.4g\n", (int)f/10, f-((int)f-(int)f%10));

Jest trochę programowy, ale przynajmniej nie zmusza cię do manipulacji na ciągach znaków.


1

Proste rozwiązanie, ale wykonuje swoje zadanie, przypisuje znaną długość i precyzję oraz pozwala uniknąć możliwości przejścia do formatu wykładniczego (co jest ryzykiem, gdy używasz% g):

// Since we are only interested in 3 decimal places, this function
// can avoid any potential miniscule floating point differences
// which can return false when using "=="
int DoubleEquals(double i, double j)
{
    return (fabs(i - j) < 0.000001);
}

void PrintMaxThreeDecimal(double d)
{
    if (DoubleEquals(d, floor(d)))
        printf("%.0f", d);
    else if (DoubleEquals(d * 10, floor(d * 10)))
        printf("%.1f", d);
    else if (DoubleEquals(d * 100, floor(d* 100)))
        printf("%.2f", d);
    else
        printf("%.3f", d);
}

Dodaj lub usuń „inne”, jeśli chcesz mieć maksymalnie 2 miejsca po przecinku; 4 miejsca po przecinku; itp.

Na przykład, jeśli chcesz 2 miejsca po przecinku:

void PrintMaxTwoDecimal(double d)
{
    if (DoubleEquals(d, floor(d)))
        printf("%.0f", d);
    else if (DoubleEquals(d * 10, floor(d * 10)))
        printf("%.1f", d);
    else
        printf("%.2f", d);
}

Jeśli chcesz określić minimalną szerokość, aby zachować wyrównanie pól, zwiększaj w razie potrzeby, na przykład:

void PrintAlignedMaxThreeDecimal(double d)
{
    if (DoubleEquals(d, floor(d)))
        printf("%7.0f", d);
    else if (DoubleEquals(d * 10, floor(d * 10)))
        printf("%9.1f", d);
    else if (DoubleEquals(d * 100, floor(d* 100)))
        printf("%10.2f", d);
    else
        printf("%11.3f", d);
}

Możesz również przekonwertować to na funkcję, w której podajesz żądaną szerokość pola:

void PrintAlignedWidthMaxThreeDecimal(int w, double d)
{
    if (DoubleEquals(d, floor(d)))
        printf("%*.0f", w-4, d);
    else if (DoubleEquals(d * 10, floor(d * 10)))
        printf("%*.1f", w-2, d);
    else if (DoubleEquals(d * 100, floor(d* 100)))
        printf("%*.2f", w-1, d);
    else
        printf("%*.3f", w, d);
}

Take d = 0.0001: then floor(d)is 0, więc różnica jest większa niż 0,000001, więc DoubleEqualsjest false, więc nie użyje "%.0f"specyfikatora: zobaczysz końcowe zera z "%*.2f"lub "%*.3f". Więc to nie odpowiada na pytanie.
Cœur,

1

Znalazłem problemy w niektórych opublikowanych rozwiązaniach. Złożyłem to na podstawie odpowiedzi powyżej. Wydaje mi się, że to działa.

int doubleEquals(double i, double j) {
    return (fabs(i - j) < 0.000001);
}

void printTruncatedDouble(double dd, int max_len) {
    char str[50];
    int match = 0;
    for ( int ii = 0; ii < max_len; ii++ ) {
        if (doubleEquals(dd * pow(10,ii), floor(dd * pow(10,ii)))) {
            sprintf (str,"%f", round(dd*pow(10,ii))/pow(10,ii));
            match = 1;
            break;
        }
    }
    if ( match != 1 ) {
        sprintf (str,"%f", round(dd*pow(10,max_len))/pow(10,max_len));
    }
    char *pp;
    int count;
    pp = strchr (str,'.');
    if (pp != NULL) {
        count = max_len;
        while (count >= 0) {
             count--;
             if (*pp == '\0')
                 break;
             pp++;
        }
        *pp-- = '\0';
        while (*pp == '0')
            *pp-- = '\0';
        if (*pp == '.') {
            *pp = '\0';
        }
    }
    printf ("%s\n", str);
}

int main(int argc, char **argv)
{
    printTruncatedDouble( -1.999, 2 ); // prints -2
    printTruncatedDouble( -1.006, 2 ); // prints -1.01
    printTruncatedDouble( -1.005, 2 ); // prints -1
    printf("\n");
    printTruncatedDouble( 1.005, 2 ); // prints 1 (should be 1.01?)
    printTruncatedDouble( 1.006, 2 ); // prints 1.01
    printTruncatedDouble( 1.999, 2 ); // prints 2
    printf("\n");
    printTruncatedDouble( -1.999, 3 ); // prints -1.999
    printTruncatedDouble( -1.001, 3 ); // prints -1.001
    printTruncatedDouble( -1.0005, 3 ); // prints -1.001 (shound be -1?)
    printTruncatedDouble( -1.0004, 3 ); // prints -1
    printf("\n");
    printTruncatedDouble( 1.0004, 3 ); // prints 1
    printTruncatedDouble( 1.0005, 3 ); // prints 1.001
    printTruncatedDouble( 1.001, 3 ); // prints 1.001
    printTruncatedDouble( 1.999, 3 ); // prints 1.999
    printf("\n");
    exit(0);
}

1

Niektóre z rozwiązań, które otrzymały najwyższe głosy, sugerują specyfikator %gkonwersji printf. To jest złe, ponieważ są przypadki, w których %gwytworzy notację naukową. Inne rozwiązania wykorzystują matematykę do drukowania żądanej liczby cyfr dziesiętnych.

Myślę, że najłatwiejszym rozwiązaniem jest użycie sprintfze specyfikatorem %fkonwersji i ręczne usunięcie końcowych zer i ewentualnie przecinka dziesiętnego z wyniku. Oto rozwiązanie C99:

#include <stdio.h>
#include <stdlib.h>

char*
format_double(double d) {
    int size = snprintf(NULL, 0, "%.3f", d);
    char *str = malloc(size + 1);
    snprintf(str, size + 1, "%.3f", d);

    for (int i = size - 1, end = size; i >= 0; i--) {
        if (str[i] == '0') {
            if (end == i + 1) {
                end = i;
            }
        }
        else if (str[i] == '.') {
            if (end == i + 1) {
                end = i;
            }
            str[end] = '\0';
            break;
        }
    }

    return str;
}

Zauważ, że znaki użyte jako cyfry i separator dziesiętny zależą od aktualnych ustawień narodowych. W powyższym kodzie przyjęto ustawienia regionalne C lub angielski (USA).


-0,00005 i 3 miejsca po przecinku dadzą „-0”, co może być pożądane lub nie. Może też uczynić „3” parametrem dla wygody innych?
delfin

0

Oto moja pierwsza próba odpowiedzi:

unieważnić
xprintfloat (format char *, float f)
{
  char s [50];
  char * p;

  sprintf (s, format, f);
  dla (p = s; * p; ++ p)
    if ('.' == * p) {
      while (* ++ p);
      while ('0' == * - p) * p = '\ 0';
    }
  printf ("% s", s);
}

Znane błędy: Możliwe przepełnienie bufora w zależności od formatu. Jeśli "." występuje z innego powodu niż% f może wystąpić zły wynik.


Twoje znane błędy są takie same, jak sama funkcja printf (), więc nie ma żadnych problemów. Ale szukałem ciągu formatu, który pozwoliłby mi zrobić to, co chciałem, a nie rozwiązania programowego, które już mam.
Gorpik

0

Dlaczego po prostu tego nie zrobić?

double f = 359.01335;
printf("%g", round(f * 1000.0) / 1000.0);

0

Niewielkie różnice w powyższym:

  1. Eliminuje okres dla przypadku (10000,0).
  2. Przerwy po pierwszym okresie są przetwarzane.

Kod tutaj:

void EliminateTrailingFloatZeros(char *iValue)
{
  char *p = 0;
  for(p=iValue; *p; ++p) {
    if('.' == *p) {
      while(*++p);
      while('0'==*--p) *p = '\0';
      if(*p == '.') *p = '\0';
      break;
    }
  }
}

Nadal ma potencjał do przepełnienia, więc bądź ostrożny; P


0

Powiedziałbym, że powinieneś użyć printf("%.8g",value);

Jeśli używasz "%.6g", nie otrzymasz żądanego wyniku dla niektórych liczb, takich jak 32.230210, powinno się wydrukować, 32.23021ale drukuje32.2302


-2

Twój kod zaokrągla do trzech miejsc po przecinku ze względu na „.3” przed f

printf("%1.3f", 359.01335);
printf("%1.3f", 359.00999);

Tak więc, jeśli druga linia została zaokrąglona do dwóch miejsc po przecinku, należy to zmienić na:

printf("%1.3f", 359.01335);
printf("%1.2f", 359.00999);

Ten kod da pożądane wyniki:

359.013
359.01

* Zauważ, że jest to założenie, że już drukujesz w oddzielnych wierszach, jeśli nie, to poniższe uniemożliwiają drukowanie w tej samej linii:

printf("%1.3f\n", 359.01335);
printf("%1.2f\n", 359.00999);

Poniższy kod źródłowy programu był moim testem dla tej odpowiedzi

#include <cstdio>

int main()
{

    printf("%1.3f\n", 359.01335);
    printf("%1.2f\n", 359.00999);

    while (true){}

    return 0;

}

4
Pytanie dotyczyło stworzenia jednego ciągu formatującego, który pomija zera końcowe, aby nie mieć różnych ciągów formatujących dla każdego przypadku.
Christoph Walesch
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.