Evite los ceros finales en printf ()

107

Sigo tropezando con los especificadores de formato para la familia de funciones printf (). Lo que quiero es poder imprimir un doble (o flotante) con un número máximo de dígitos después del punto decimal. Si uso:

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

yo obtengo

359.013
359.010

En lugar de lo deseado

359.013
359.01

Alguien puede ayudarme?

Gorpik
fuente
1
La inexactitud del punto flotante realmente significa que usted mismo debe hacer el redondeo. Tome una variante de la respuesta de R y Juha (que no maneja bien los ceros finales) y arréglela.
wnoise

Respuestas:

88

Esto no se puede hacer con los printfespecificadores de formato normales . Lo más cercano que podría obtener sería:

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

pero ".6" es el ancho numérico total, por lo que

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

lo rompe.

Lo que puede hacer es colocar sprintf("%.20g")el número en un búfer de cadena y luego manipular la cadena para que solo tenga N caracteres más allá del punto decimal.

Suponiendo que su número está en la variable num, la siguiente función eliminará todos los Ndecimales menos los primeros , luego eliminará los ceros finales (y el punto decimal si fueran todos ceros).

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

Si no está satisfecho con el aspecto de truncamiento (que se convertiría 0.12399en en 0.123lugar de redondearlo 0.124), puede utilizar las funciones de redondeo que ya proporciona printf. Solo necesita analizar el número de antemano para crear dinámicamente los anchos, luego usarlos para convertir el número en una cadena:

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

En nDecimals()este caso, el objetivo es calcular correctamente los anchos de campo y luego formatear el número usando una cadena de formato basada en eso. El arnés de prueba main()muestra esto en acción:

  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 vez que tenga el valor redondeado correctamente, puede volver a pasarlo morphNumericString()para eliminar los ceros finales simplemente cambiando:

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

dentro:

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

(o llamando morphNumericStringal final de nDecimalspero, en ese caso, probablemente combinaría los dos en una función), y terminas 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
paxdiablo
fuente
8
Tenga en cuenta que esta respuesta asume que el lugar decimal es. .En algunos lugares, el lugar decimal es en realidad una ,coma.
1
En realidad, hay un error tipográfico menor aquí: p = strchr (str, '.'); en realidad debería ser p = strchr (s, '.'); Para usar la función param en lugar de la var global.
n3wtz
El uso de demasiados dígitos puede generar resultados inesperados ... por ejemplo, "% .20g" con 0.1 mostrará basura. Lo que debe hacer si no quiere sorprender a los usuarios es comenzar con, digamos, 15 dígitos y seguir aumentando hasta atofque devuelva el mismo valor.
6502
@ 6502, eso se debe a que no existe 0.1 en IEEE754 :-) Sin embargo, no causará ningún problema (al menos por ese valor) ya que el resultado 0.10000000000000000555será 0.1cuando se elimine. El problema puede surgir si tiene un valor ligeramente inferior, como si la representación más cercana de 42.1fue 42.099999999314159. Si realmente quisiera manejar eso, entonces probablemente necesitaría redondear en función del último dígito eliminado en lugar de truncar.
paxdiablo
1
@paxdiablo: No es necesario crear dinámicamente una cadena de formato para printffunciones similares, ya que ya admiten parámetros dinámicos ( *carácter especial): por ejemplo, printf("%*.*f", total, decimals, x);genera un número con la longitud total del campo y los decimales especificados dinámicamente.
6502
54

Para deshacerse de los ceros finales, debe usar el formato "% g":

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

Después de aclarar un poco la pregunta, la supresión de ceros no es lo único que se preguntó, sino que también se requirió limitar la salida a tres lugares decimales. Creo que eso no se puede hacer solo con cadenas de formato sprintf. Como señaló Pax Diablo , se requeriría manipulación de cuerdas.

Tomalak
fuente
Consulte la página de ayuda de MSDN: msdn.microsoft.com/en-us/library/0ecbz014(VS.80).aspx
xtofl
@Tomalak: Esto no hace lo que quiero. Quiero poder especificar un número máximo de dígitos después del punto decimal. Esto es: quiero que 1.3347 se imprima "1.335" y 1.3397 se imprima "1.34" @xtofl: Ya lo había verificado, pero todavía no puedo ver la respuesta a mi problema
Gorpik
3
El autor quiere estilo 'f' no estilo 'e'. 'g' puede usar el estilo 'e': De la documentación: El estilo e se usa si el exponente de su conversión es menor que -4 o mayor o igual que la precisión.
Julien Palard
20

Me gusta la respuesta de R. modificada ligeramente:

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

'1000' determina la precisión.

Potencia al redondeo de 0,5.

EDITAR

Ok, esta respuesta fue editada varias veces y perdí la noción de lo que estaba pensando hace unos años (y originalmente no cumplía con todos los criterios). Entonces, aquí hay una nueva versión (que llena todos los criterios y maneja los números negativos correctamente):

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

Y los casos de prueba:

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

Y el resultado de las pruebas:

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

Ahora, se cumplen todos los criterios:

  • el número máximo de decimales detrás del cero es fijo
  • se eliminan los ceros finales
  • lo hace matemáticamente correcto (¿verdad?)
  • funciona (ahora) también cuando el primer decimal es cero

Desafortunadamente, esta respuesta es de dos líneas, ya sprintfque no devuelve la cadena.

Juha
fuente
1
Sin embargo, esto no parece recortar los ceros finales. Con mucho gusto puede escupir algo así como 1.1000.
Dean J
La pregunta y mi respuesta ya no son las originales ... lo comprobaré más tarde.
Juha
1
Casos de prueba fallidos: 1.012 con formateador "% .2g" dará como resultado 1.012 en lugar de 1.01.
wtl
@wtl Buena captura. Ahora arreglado.
Juha
@Jeroen: Creo que los flotadores también fallan en algún momento cuando los números crecen ... pero básicamente el encasillado a long, long long o int64 debería funcionar.
Juha
2

Busco en la cadena (comenzando más a la derecha) el primer carácter en el rango 1a 9(valor ASCII 49- 57) luego null(establecido en 0) cada carácter a la derecha - ver a continuación:

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;
    }
}
DaveR
fuente
2

¿Qué pasa con algo como esto (puede haber errores de redondeo y problemas de valor negativo que necesitan depuración, se deja como un ejercicio para el lector):

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

Es un poco programático, pero al menos no te obliga a manipular cadenas.

R .. GitHub DEJA DE AYUDAR A ICE
fuente
1

Una solución simple pero que hace el trabajo, asigna una longitud y precisión conocidas y evita la posibilidad de pasar al formato exponencial (que es un riesgo cuando se usa% 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);
}

Agregue o elimine "más" si desea un máximo de 2 decimales; 4 decimales; etc.

Por ejemplo, si desea 2 decimales:

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

Si desea especificar el ancho mínimo para mantener los campos alineados, incremente según sea necesario, por ejemplo:

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

También puede convertir eso en una función en la que pase el ancho deseado 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);
}
Iaijutsu
fuente
Tome d = 0.0001: luego floor(d)es 0, por lo que la diferencia es mayor que 0.000001, por lo que DoubleEqualses falso, por lo que no usará el "%.0f"especificador: verá ceros al final de "%*.2f"o "%*.3f". Entonces no responde la pregunta.
Cœur
1

Encontré problemas en algunas de las soluciones publicadas. Reuní esto según las respuestas anteriores. Parece funcionar para mí.

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);
}
magnusviri
fuente
1

Algunas de las soluciones más votadas sugieren el %gespecificador de conversión de printf. Esto es incorrecto porque hay casos en los %gque producirá notación científica. Otras soluciones usan matemáticas para imprimir el número deseado de dígitos decimales.

Creo que la solución más fácil es usarla sprintfcon el %fespecificador de conversión y eliminar manualmente los ceros finales y posiblemente un punto decimal del resultado. Aquí hay una solución 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;
}

Tenga en cuenta que los caracteres utilizados para los dígitos y el separador decimal dependen de la configuración regional actual. El código anterior asume una configuración regional C o inglés de EE. UU.

Nwellnhof
fuente
−0,00005 y 3 decimales producirán "−0", lo que puede ser deseable o no. Además, ¿quizás convertir "3" en un parámetro para conveniencia de otros?
delfín
0

Aquí está mi primer intento de respuesta:

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

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

Errores conocidos: posible desbordamiento del búfer según el formato. Si "." está presente por otra razón que% f puede ocurrir un resultado incorrecto.


fuente
Sus errores conocidos son los mismos que tiene printf (), por lo que no hay problemas allí. Pero estaba buscando una cadena de formato que me permitiera hacer lo que quería, no una solución programática, que es lo que ya tengo.
Gorpik
0

¿Por qué no hacer esto?

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

Ligera variación de arriba:

  1. Elimina el período por caso (10000.0).
  2. Pausas después de que se procesa el primer período.

Codifique aquí:

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

Todavía tiene potencial de desbordamiento, así que tenga cuidado; P

David Thornley
fuente
0

Yo diría que deberías usar printf("%.8g",value);

Si lo usa "%.6g", no obtendrá la salida deseada para algunos números como 32.230210, debería imprimirse 32.23021pero imprime32.2302

Ankit Mishra
fuente
-2

Su código se redondea a tres lugares decimales debido al ".3" antes de la f

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

Por lo tanto, si redondeó la segunda línea a dos lugares decimales, debe cambiarla a esto:

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

Ese código generará los resultados deseados:

359.013
359.01

* Tenga en cuenta que esto es asumiendo que ya lo tiene imprimiendo en líneas separadas, de lo contrario, lo siguiente evitará que se imprima en la misma línea:

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

El siguiente código fuente del programa fue mi prueba para esta respuesta

#include <cstdio>

int main()
{

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

    while (true){}

    return 0;

}
TeamXlink
fuente
4
La pregunta era crear una cadena de formato que omita los ceros finales, no tener cadenas de formato diferentes para cada caso.
Christoph Walesch