Évitez les zéros de fin dans printf ()

107

Je n'arrête pas de tomber sur les spécificateurs de format pour la famille de fonctions printf (). Ce que je veux, c'est pouvoir imprimer un double (ou flottant) avec un nombre maximum de chiffres donné après la virgule décimale. Si j'utilise:

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

Je reçois

359.013
359.010

Au lieu du désir

359.013
359.01

Quelqu'un peut-il m'aider?

Gorpik
la source
1
L'inexactitude de la virgule flottante signifie vraiment que vous devez arrondir vous-même. Prenez une variante de la réponse de R et Juha (qui ne gère pas tout à fait les zéros de fin) et corrigez-la.
wnoise

Réponses:

88

Cela ne peut pas être fait avec les printfspécificateurs de format normaux . Le plus proche que vous pourriez obtenir serait:

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

mais le ".6" est la largeur numérique totale, donc

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

le casse.

Ce que vous pouvez faire est de placer sprintf("%.20g")le nombre dans un tampon de chaîne, puis de manipuler la chaîne pour n'avoir que N caractères après la virgule décimale.

En supposant que votre nombre se trouve dans la variable num, la fonction suivante supprimera toutes les Ndécimales sauf les premières , puis supprimera les zéros de fin (et le point décimal s'ils étaient tous des zéros).

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 vous n'êtes pas satisfait de l'aspect troncature (qui tournerait 0.12399en au 0.123lieu d'arrondir à 0.124), vous pouvez réellement utiliser les installations déjà fournies par arrondissement printf. Il vous suffit d'analyser le nombre au préalable pour créer dynamiquement les largeurs, puis de les utiliser pour transformer le nombre en chaîne:

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

L'intérêt de nDecimals()dans ce cas est de calculer correctement les largeurs de champ, puis de formater le nombre en utilisant une chaîne de format basée sur cela. Le harnais de test main()montre cela en action:

  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

Une fois que vous avez la valeur correctement arrondie, vous pouvez à nouveau la passer à morphNumericString()pour supprimer les zéros de fin en modifiant simplement:

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

dans:

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

(ou appeler morphNumericStringà la fin de nDecimalsmais, dans ce cas, je combinerais probablement les deux en une seule fonction), et vous vous retrouverez avec:

  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
la source
8
Notez que cette réponse suppose que la décimale est .dans certains paramètres régionaux, la décimale est en fait une ,virgule.
1
Il y a en fait une petite faute de frappe ici - p = strchr (str, '.'); devrait en fait être p = strchr (s, '.'); Pour utiliser la fonction param plutôt que la variable globale.
n3wtz
Utiliser trop de chiffres peut déclencher des résultats inattendus ... par exemple "% .20g" avec 0.1 affichera des déchets. Ce que vous devez faire si vous ne voulez pas surprendre les utilisateurs, c'est de commencer par disons 15 chiffres et de continuer à incrémenter jusqu'à ce que vous atofrestituiez la même valeur.
6502
@ 6502, c'est parce qu'il n'y a pas de 0,1 dans IEEE754 :-) Cependant, cela ne posera pas de problème (au moins pour cette valeur) puisque le résultat 0.10000000000000000555sera une 0.1fois supprimé. Le problème peut survenir si vous avez une valeur légèrement inférieure, par exemple si la représentation la plus proche de 42.1était 42.099999999314159. Si vous vouliez vraiment gérer cela, vous auriez probablement besoin d'arrondir en fonction du dernier chiffre supprimé plutôt que de tronquer.
paxdiablo
1
@paxdiablo: Il n'est pas nécessaire de créer dynamiquement une chaîne de format pour les printffonctions semblables car elles prennent déjà en charge les paramètres dynamiques ( *caractère spécial): par exemple, printf("%*.*f", total, decimals, x);génère un nombre avec la longueur totale du champ et les décimales spécifiées dynamiquement.
6502
54

Pour supprimer les zéros de fin, vous devez utiliser le format "% g":

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

Une fois la question clarifiée un peu, la suppression des zéros n'est pas la seule chose qui a été posée, mais la limitation de la sortie à trois décimales était également nécessaire. Je pense que cela ne peut pas être fait avec des chaînes au format sprintf seules. Comme Pax Diablo l'a souligné, une manipulation des cordes serait nécessaire.

Tomalak
la source
Consultez la page d'aide MSDN: msdn.microsoft.com/en-us/library/0ecbz014(VS.80).aspx
xtofl
@Tomalak: Cela ne fait pas ce que je veux. Je veux pouvoir spécifier un nombre maximum de chiffres après la virgule décimale. C'est: je veux que 1.3347 soit imprimé "1.335" et 1.3397 soit imprimé "1.34" @xtofl: j'avais déjà vérifié cela, mais je ne vois toujours pas la réponse à mon problème
Gorpik
3
L'auteur veut le style «f» et non le style «e». 'g' peut utiliser le style 'e': De la documentation: Le style e est utilisé si l'exposant de sa conversion est inférieur à -4 ou supérieur ou égal à la précision.
Julien Palard
20

J'aime la réponse de R. légèrement modifiée:

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

«1000» détermine la précision.

Puissance à l'arrondi 0,5.

ÉDITER

Ok, cette réponse a été éditée plusieurs fois et j'ai perdu la trace de ce à quoi je pensais il y a quelques années (et à l'origine elle ne remplissait pas tous les critères). Voici donc une nouvelle version (qui remplit tous les critères et gère correctement les nombres négatifs):

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

Et les cas de 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;
}

Et la sortie des tests:

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

Désormais, tous les critères sont remplis:

  • le nombre maximum de décimales derrière le zéro est fixé
  • les zéros de fin sont supprimés
  • il le fait mathématiquement (non?)
  • fonctionne (maintenant) aussi lorsque la première décimale est égale à zéro

Malheureusement, cette réponse est une double ligne car sprintfelle ne renvoie pas la chaîne.

Juha
la source
1
Cela ne semble pas réellement couper les zéros de fin, cependant? Il peut volontiers cracher quelque chose comme 1.1000.
Dean J
La question et ma réponse ne sont plus les originaux ... Je vérifierai plus tard.
Juha
1
Scénarios de test ayant échoué: 1.012 avec le formateur "% .2g" donnera 1.012 au lieu de 1.01.
wtl
@wtl Belle prise. Maintenant corrigé.
Juha le
@Jeroen: Je pense que les flottants échouent également à un moment donné lorsque les nombres deviennent plus gros ... mais fondamentalement, le transtypage en long, long long ou int64 devrait fonctionner.
Juha
2

Je recherche dans la chaîne (commençant à l'extrême droite) le premier caractère de la plage 1jusqu'à 9(valeur ASCII 49- 57) puis null(défini sur 0) chaque caractère à droite - voir ci-dessous:

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
la source
2

Qu'en est-il de quelque chose comme ça (peut avoir des erreurs d'arrondi et des problèmes de valeur négative qui nécessitent un débogage, laissés comme exercice pour le lecteur):

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

C'est légèrement programmatique mais au moins cela ne vous oblige pas à manipuler des chaînes.

R .. GitHub STOP AIDING ICE
la source
1

Une solution simple mais qui fait le travail, attribue une longueur et une précision connues et évite le risque de passer au format exponentiel (ce qui est un risque lorsque vous utilisez% 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);
}

Ajoutez ou supprimez "elses" si vous voulez un maximum de 2 décimales; 4 décimales; etc.

Par exemple si vous vouliez 2 décimales:

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 vous souhaitez spécifier la largeur minimale pour que les champs restent alignés, incrémentez si nécessaire, par exemple:

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

Vous pouvez également convertir cela en une fonction dans laquelle vous transmettez la largeur souhaitée du champ:

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
la source
Prenez d = 0.0001: alors floor(d)est 0, donc la différence est plus grande que 0,000001, donc DoubleEqualsest faux, donc il n'utilisera pas le "%.0f"spécificateur: vous verrez des zéros à la fin de "%*.2f"ou "%*.3f". Cela ne répond donc pas à la question.
Cœur du
1

J'ai trouvé des problèmes dans certaines des solutions affichées. J'ai mis cela en fonction des réponses ci-dessus. Il semble fonctionner pour moi.

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
la source
1

Certaines des solutions les plus votées suggèrent le %gspécificateur de conversion de printf. C'est faux car il y a des cas où%g produira une notation scientifique. D'autres solutions utilisent les mathématiques pour imprimer le nombre souhaité de chiffres décimaux.

Je pense que la solution la plus simple est d'utiliser sprintfavec le %fspécificateur de conversion et de supprimer manuellement les zéros de fin et éventuellement un point décimal du résultat. Voici une solution 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;
}

Notez que les caractères utilisés pour les chiffres et le séparateur décimal dépendent des paramètres régionaux actuels. Le code ci-dessus suppose un langage C ou anglais américain.

nwellnhof
la source
−0,00005 et 3 décimales donneront "−0" qui peut être souhaité ou non. Aussi, peut-être faire de "3" un paramètre pour la commodité des autres?
dauphin le
0

Voici mon premier essai de réponse:

néant
xprintfloat (format char *, float f)
{
  char s [50];
  char * p;

  sprintf (s, format, f);
  pour (p = s; * p; ++ p)
    si ('.' == * p) {
      tandis que (* ++ p);
      while ('0' == * - p) * p = '\ 0';
    }
  printf ("% s", s);
}

Bogues connus: Dépassement de tampon possible selon le format. Si "." est présent pour une raison autre que% f un mauvais résultat peut se produire.


la source
Vos bogues connus sont les mêmes que printf () lui-même, donc pas de problème. Mais je cherchais une chaîne de format qui me permettait de faire ce que je voulais, pas une solution programmatique, ce que j'ai déjà.
Gorpik
0

Pourquoi ne pas faire ça?

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

Légère variation sur ci-dessus:

  1. Élimine la période pour le cas (10000.0).
  2. Les pauses après la première période sont traitées.

Code ici:

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

Il a encore un potentiel de débordement, alors soyez prudent; P

David Thornley
la source
0

Je dirais que vous devriez utiliser printf("%.8g",value);

Si vous utilisez, "%.6g"vous n'obtiendrez pas la sortie souhaitée pour certains nombres comme 32.230210, il devrait imprimer 32.23021mais il s'imprime32.2302

Ankit Mishra
la source
-2

Votre code est arrondi à trois décimales en raison du ".3" avant le f

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

Ainsi, si la deuxième ligne est arrondie à deux décimales, vous devez la changer comme suit:

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

Ce code affichera les résultats souhaités:

359.013
359.01

* Notez que cela suppose que vous l'avez déjà imprimé sur des lignes séparées, sinon, les éléments suivants l'empêcheront d'imprimer sur la même ligne:

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

Le code source du programme suivant a été mon test pour cette réponse

#include <cstdio>

int main()
{

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

    while (true){}

    return 0;

}
TeamXlink
la source
4
La question était de créer une chaîne de format qui omet les zéros de fin, pour ne pas avoir de chaînes de format différentes pour chaque cas.
Christoph Walesch