Evita gli zeri finali in printf ()


107

Continuo a inciampare sugli specificatori di formato per la famiglia di funzioni printf (). Quello che voglio è essere in grado di stampare un double (o float) con un numero massimo di cifre dato dopo il punto decimale. Se uso:

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

ottengo

359.013
359.010

Invece del desiderato

359.013
359.01

Qualcuno può aiutarmi?


1
L'inesattezza in virgola mobile significa davvero che dovresti fare l'arrotondamento da solo. Prendi una variante della risposta di R e Juha (che non gestisce abbastanza gli zeri finali) e aggiustala.
wnoise

Risposte:


88

Questo non può essere fatto con i normali printfidentificatori di formato. Il più vicino che potresti ottenere sarebbe:

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

ma ".6" è la larghezza numerica totale così

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

lo rompe.

Quello che puoi fare è inseriresprintf("%.20g") il numero in un buffer di stringa, quindi manipolare la stringa per avere solo N caratteri oltre il punto decimale.

Supponendo che il tuo numero sia nella variabile num, la seguente funzione rimuoverà tutti i Ndecimali tranne i primi , quindi rimuoverà gli zeri finali (e il punto decimale se fossero tutti zeri).

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

Se non sei soddisfatto dell'aspetto del troncamento (che si trasformerebbe 0.12399in 0.123anziché arrotondarlo a 0.124), puoi effettivamente utilizzare le funzionalità di arrotondamento già fornite da printf. Devi solo analizzare il numero in anticipo per creare dinamicamente le larghezze, quindi usarle per trasformare il numero in una stringa:

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

Il punto centrale nDecimals()in questo caso è calcolare correttamente le larghezze del campo, quindi formattare il numero utilizzando una stringa di formato basata su quella. Il test harness main()mostra questo in azione:

  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

Una volta ottenuto il valore arrotondato correttamente, puoi passarlo di nuovo a morphNumericString()per rimuovere gli zeri finali semplicemente cambiando:

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

in:

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

(o chiamando morphNumericStringalla fine di nDecimalsma, in tal caso, probabilmente combinerei semplicemente i due in una funzione), e finisci con:

  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
Nota, questa risposta presuppone che la posizione decimale sia .In alcune impostazioni locali, la posizione decimale è in realtà una ,virgola.

1
In realtà c'è un errore di battitura minore qui - p = strchr (str, '.'); dovrebbe effettivamente essere p = strchr (s, '.'); Per utilizzare la funzione param piuttosto che la variabile globale.
n3wtz

L'uso di troppe cifre potrebbe generare risultati imprevisti ... ad esempio "% .20g" con 0.1 visualizzerà spazzatura. Quello che devi fare se non vuoi sorprendere gli utenti è iniziare da diciamo 15 cifre e continuare a incrementare fino a atofrestituire lo stesso valore.
6502

@ 6502, perché non esiste 0.1 in IEEE754 :-) Tuttavia, non causerà problemi (almeno per quel valore) poiché la risultante 0.10000000000000000555sarà 0.1quando rimossa. Il problema potrebbe sorgere se si dispone di un valore leggermente inferiore, ad esempio se la rappresentazione più vicina di 42.1era 42.099999999314159. Se volessi davvero gestirlo, probabilmente dovresti arrotondare in base all'ultima cifra rimossa anziché troncare.
paxdiablo

1
@paxdiablo: non è necessario creare dinamicamente una stringa di formato per le printffunzioni -like poiché supportano già parametri dinamici ( *carattere speciale): ad esempio printf("%*.*f", total, decimals, x);restituisce un numero con la lunghezza totale del campo e i decimali specificati dinamicamente.
6502

54

Per eliminare gli zeri finali, dovresti utilizzare il formato "% g":

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

Dopo che la domanda è stata chiarita un po ', la soppressione degli zeri non è l'unica cosa che è stata richiesta, ma è stato anche necessario limitare l'output a tre cifre decimali. Penso che non possa essere fatto con le sole stringhe di formato sprintf. Come ha sottolineato Pax Diablo , sarebbe necessaria la manipolazione delle corde.



@ Tomalak: questo non fa quello che voglio. Voglio essere in grado di specificare un numero massimo di cifre dopo il punto decimale. Questo è: voglio che 1.3347 venga stampato "1.335" e 1.3397 che venga stampato "1.34" @xtofl: lo avevo già controllato, ma non riesco ancora a vedere la risposta al mio problema
Gorpik

3
L'autore vuole lo stile "f" non lo stile "e". 'g' può usare lo stile 'e': Dalla documentazione: lo stile e è usato se l'esponente della sua conversione è minore di -4 o maggiore o uguale alla precisione.
Julien Palard

20

Mi piace la risposta di R. leggermente modificata:

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

"1000" determina la precisione.

Potenza all'arrotondamento di 0,5.

MODIFICARE

Ok, questa risposta è stata modificata alcune volte e ho perso traccia di quello che pensavo qualche anno fa (e originariamente non soddisfaceva tutti i criteri). Quindi ecco una nuova versione (che riempie tutti i criteri e gestisce correttamente i numeri negativi):

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

E i casi di test:

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

E l'output dei test:

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

Ora, tutti i criteri sono soddisfatti:

  • il numero massimo di decimali dietro lo zero è fisso
  • gli zeri finali vengono rimossi
  • lo fa matematicamente giusto (giusto?)
  • funziona (ora) anche quando il primo decimale è zero

Purtroppo questa risposta è a due righe in quanto sprintfnon restituisce la stringa.


1
Questo in realtà non sembra tagliare gli zeri finali, però? Può sputare volentieri qualcosa come 1.1000.
Dean J

La domanda e la mia risposta non sono più gli originali ... controllerò più tardi.
Juha

1
Casi di test non riusciti: 1.012 con formattatore "% .2g" restituirà 1.012 anziché 1.01.
wtl

@wtl Bella cattura. Ora risolto.
Juha

@ Jeroen: Penso che i float falliscono anche a un certo punto quando i numeri diventano più grandi ... ma fondamentalmente il typecasting su long, long long o int64 dovrebbe funzionare.
Juha

2

Cerco la stringa (iniziando più a destra) per il primo carattere nell'intervallo 1fino a 9(valore ASCII 49- 57) quindi null(impostato su 0) ogni carattere a destra di esso - vedi sotto:

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

Che ne dici di qualcosa di simile (potrebbe avere errori di arrotondamento e problemi di valore negativo che richiedono il debug, lasciato come esercizio per il lettore):

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

È leggermente programmatico ma almeno non ti fa fare alcuna manipolazione delle stringhe.


1

Una soluzione semplice ma che porta a termine il lavoro, assegna una lunghezza e una precisione note ed evita la possibilità di ottenere un formato esponenziale (che è un rischio quando usi% 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);
}

Aggiungi o rimuovi "elses" se vuoi un massimo di 2 decimali; 4 decimali; eccetera.

Ad esempio, se volessi 2 decimali:

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

Se si desidera specificare la larghezza minima per mantenere i campi allineati, aumentare se necessario, ad esempio:

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

Puoi anche convertirlo in una funzione in cui passi la larghezza desiderata del campo:

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

Prendi d = 0.0001: allora floor(d)è 0, quindi la differenza è maggiore di 0,000001, quindi DoubleEqualsè falso, quindi non utilizzerà lo "%.0f"specificatore: vedrai gli zeri finali da "%*.2f"o "%*.3f". Quindi non risponde alla domanda.
Cœur

1

Ho riscontrato problemi in alcune delle soluzioni pubblicate. Ho messo insieme questo in base alle risposte sopra. Sembra funzionare per me.

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

Alcune delle soluzioni più votate suggeriscono lo %gspecificatore di conversione di printf. Questo è sbagliato perché ci sono casi in cui%g produrrà notazione scientifica. Altre soluzioni utilizzano la matematica per stampare il numero desiderato di cifre decimali.

Penso che la soluzione più semplice sia quella di utilizzare sprintfcon lo %fspecificatore di conversione e rimuovere manualmente gli zeri finali e possibilmente un punto decimale dal risultato. Ecco una soluzione 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;
}

Notare che i caratteri utilizzati per le cifre e il separatore decimale dipendono dalle impostazioni internazionali correnti. Il codice precedente presuppone un'impostazione internazionale C o inglese americano.


−0,00005 e 3 decimali restituiranno "−0" che può o non può essere desiderato. Inoltre, forse rendere "3" un parametro per la comodità degli altri?
delfino

0

Ecco il mio primo tentativo di risposta:

vuoto
xprintfloat (formato char *, float f)
{
  char s [50];
  char * p;

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

Bug noti: possibile overflow del buffer a seconda del formato. Se "." è presente per un motivo diverso da quello in cui% f potrebbe verificarsi un risultato errato.


I tuoi bug conosciuti sono gli stessi che ha printf (), quindi nessun problema. Ma stavo cercando una stringa di formato che mi permettesse di fare ciò che volevo, non una soluzione programmatica, che è ciò che ho già.
Gorpik

0

Perché non farlo semplicemente?

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

0

Leggera variazione sopra:

  1. Elimina il periodo per il caso (10000.0).
  2. Le pause dopo l'elaborazione del primo periodo.

Codice qui:

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

Ha ancora il potenziale per overflow, quindi fai attenzione; P


0

Direi che dovresti usare printf("%.8g",value);

Se si utilizza "%.6g"non si otterrà l'output desiderato per alcuni numeri come 32.230210 dovrebbe essere stampato 32.23021ma viene stampato32.2302


-2

Il codice viene arrotondato a tre cifre decimali a causa del ".3" prima della f

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

Quindi, se la seconda riga è arrotondata a due cifre decimali, dovresti cambiarla in questo:

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

Quel codice produrrà i risultati desiderati:

359.013
359.01

* Nota che questo presuppone che tu stia già stampando su righe separate, altrimenti quanto segue gli impedirà di stampare sulla stessa riga:

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

Il seguente codice sorgente del programma è stato il mio test per questa risposta

#include <cstdio>

int main()
{

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

    while (true){}

    return 0;

}

4
La domanda riguardava la creazione di una stringa di formato che ometta gli zeri finali, per non avere stringhe di formato diverse per ogni caso.
Christoph Walesch
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.