Hindari membuntuti nol di printf ()


107

Saya terus tersandung pada penentu format untuk keluarga fungsi printf (). Yang saya inginkan adalah bisa mencetak ganda (atau float) dengan jumlah digit maksimum setelah koma desimal. Jika saya menggunakan:

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

saya mendapat

359.013
359.010

Bukan yang diinginkan

359.013
359.01

Adakah yang bisa membantu saya?


1
Ketidakakuratan floating point berarti Anda harus melakukan pembulatan sendiri. Ambil varian jawaban R dan Juha (yang tidak cukup menangani angka nol yang tertinggal), dan perbaiki.
wnoise

Jawaban:


88

Ini tidak dapat dilakukan dengan penentu printfformat normal . Yang paling dekat yang bisa Anda dapatkan adalah:

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

tapi ".6" adalah lebar numerik total jadi

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

hancurkan.

Apa yang dapat Anda lakukan adalah sprintf("%.20g")memberikan nomor ke buffer string kemudian memanipulasi string tersebut agar hanya memiliki karakter N setelah titik desimal.

Dengan asumsi bilangan Anda ada di variabel num, fungsi berikut akan menghapus semua kecuali Ndesimal pertama , lalu menghapus angka nol yang tertinggal (dan titik desimal jika semuanya nol).

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

Jika Anda tidak senang dengan aspek pemotongan (yang akan berubah 0.12399menjadi 0.123daripada membulatkannya 0.124), Anda sebenarnya dapat menggunakan fasilitas pembulatan yang sudah disediakan oleh printf. Anda hanya perlu menganalisis angka sebelumnya untuk membuat lebar secara dinamis, lalu gunakan angka tersebut untuk mengubah angka menjadi string:

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

Inti dari nDecimals()kasus ini adalah untuk menghitung lebar bidang dengan benar, lalu memformat angka menggunakan format string berdasarkan itu. Harness uji main()menunjukkan ini dalam tindakan:

  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

Setelah Anda memiliki nilai yang dibulatkan dengan benar, Anda dapat sekali lagi meneruskannya ke morphNumericString()untuk menghapus nol di belakang hanya dengan mengubah:

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

ke:

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

(atau memanggil morphNumericStringdi akhir nDecimalstapi, dalam hal ini, saya mungkin hanya menggabungkan keduanya menjadi satu fungsi), dan Anda berakhir dengan:

  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
Perhatikan, jawaban ini mengasumsikan bahwa tempat desimal adalah .Di beberapa lokal, tempat desimal sebenarnya adalah ,koma.

1
Sebenarnya ada kesalahan ketik kecil di sini - p = strchr (str, '.'); seharusnya benar-benar menjadi p = strchr (s, '.'); Untuk menggunakan fungsi param daripada global var.
n3wtz

Menggunakan terlalu banyak digit dapat memicu hasil yang tidak diharapkan ... misalnya "% .20g" dengan 0.1 akan menampilkan sampah. Yang perlu Anda lakukan jika Anda tidak ingin mengejutkan pengguna adalah mulai dari katakanlah 15 digit dan terus bertambah hingga atofmemberikan kembali nilai yang sama.
6502

@ 6502, itu karena tidak ada hal seperti 0,1 di IEEE754 :-) Namun, itu tidak akan menimbulkan masalah (setidaknya untuk nilai itu) karena resultan 0.10000000000000000555akan menjadi 0.1ketika dilucuti kembali. Masalahnya mungkin muncul jika Anda memiliki nilai sedikit di bawah seperti jika representasi terdekat dari 42.1was 42.099999999314159. Jika Anda benar-benar ingin mengatasinya, maka Anda mungkin perlu membulatkan berdasarkan digit terakhir yang dihapus daripada dipotong.
paxdiablo

1
@paxdiablo: Tidak perlu membuat string format secara dinamis untuk printffungsi -like karena mereka sudah mendukung parameter dinamis ( *karakter khusus): misalnya printf("%*.*f", total, decimals, x);mengeluarkan angka dengan panjang bidang dan desimal total yang ditentukan secara dinamis.
6502

54

Untuk menghilangkan nol di belakangnya, Anda harus menggunakan format "% g":

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

Setelah pertanyaan diklarifikasi sedikit, bahwa menekan angka nol bukan satu-satunya hal yang ditanyakan, tetapi membatasi keluaran ke tiga tempat desimal juga diperlukan. Saya pikir itu tidak bisa dilakukan dengan string format sprintf saja. Seperti yang ditunjukkan Pax Diablo , manipulasi string akan diperlukan.



@Tomalak: Ini tidak melakukan apa yang saya inginkan. Saya ingin dapat menentukan jumlah digit maksimum setelah koma desimal. Ini adalah: Saya ingin 1.3347 dicetak "1.335" dan 1.3397 dicetak "1.34" @xtofl: Saya sudah memeriksanya, tetapi saya masih tidak bisa melihat jawaban untuk masalah saya
Gorpik

3
Penulis menginginkan gaya 'f' bukan gaya 'e'. 'g' dapat menggunakan gaya 'e': Dari dokumentasi: Gaya e digunakan jika eksponen dari konversinya kurang dari -4 atau lebih besar dari atau sama dengan presisi.
Julien Palard

20

Saya suka jawaban R. sedikit diubah:

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

'1000' menentukan presisi.

Kekuatan ke pembulatan 0,5.

EDIT

Oke, jawaban ini diedit beberapa kali dan saya kehilangan jejak apa yang saya pikirkan beberapa tahun yang lalu (dan awalnya tidak memenuhi semua kriteria). Jadi inilah versi baru (yang memenuhi semua kriteria dan menangani angka negatif dengan benar):

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

Dan kasus uji:

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

Dan hasil dari tes:

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

Sekarang, semua kriteria terpenuhi:

  • jumlah maksimum desimal di belakang nol tetap
  • nol tertinggal dihapus
  • itu benar secara matematis (kan?)
  • berfungsi (sekarang) juga ketika desimal pertama adalah nol

Sayangnya jawaban ini adalah dua baris karena sprintftidak mengembalikan string.


1
Ini sebenarnya tidak memangkas angka nol yang tertinggal? Ini dengan senang hati dapat mengeluarkan sesuatu seperti 1.1000.
Dekan J

Pertanyaan dan jawaban saya bukan yang asli lagi ... akan saya periksa nanti.
Juha

1
Kasus uji yang gagal: 1.012 dengan pemformat "% .2g" akan menghasilkan 1.012, bukan 1.01.
wtl

@wtl Tangkapan bagus. Sekarang sudah diperbaiki.
Juha

@ Jeroen: Saya pikir float gagal juga di beberapa titik ketika angka menjadi lebih besar ... tetapi pada dasarnya typecasting menjadi long, long long atau int64 harus berfungsi.
Juha

2

Saya mencari string (mulai paling kanan) untuk karakter pertama dalam rentang 1ke 9(nilai ASCII 49- 57) lalu null(setel ke 0) setiap karakter di kanannya - lihat di bawah:

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

Bagaimana dengan hal seperti ini (mungkin memiliki kesalahan pembulatan dan masalah nilai negatif yang perlu di-debug, ditinggalkan sebagai latihan untuk pembaca):

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

Ini sedikit terprogram tetapi setidaknya tidak membuat Anda melakukan manipulasi string apa pun.


1

Sebuah solusi sederhana tetapi menyelesaikan pekerjaan, menetapkan panjang dan presisi yang diketahui dan menghindari kemungkinan format eksponensial (yang merupakan risiko jika Anda menggunakan% 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);
}

Tambahkan atau hapus "lain" jika Anda menginginkan maksimal 2 desimal; 4 desimal; dll.

Misalnya jika Anda menginginkan 2 desimal:

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

Jika Anda ingin menentukan lebar minimum untuk menjaga bidang sejajar, tambahkan sesuai kebutuhan, misalnya:

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

Anda juga dapat mengonversinya menjadi fungsi di mana Anda meneruskan lebar bidang yang diinginkan:

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

Ambil d = 0.0001: kemudian floor(d)adalah 0, sehingga perbedaannya adalah lebih besar dari 0.000001, sehingga DoubleEqualsadalah palsu, sehingga akan tidak menggunakan "%.0f"specifier: Anda akan melihat membuntuti nol dari "%*.2f"atau "%*.3f". Jadi itu tidak menjawab pertanyaan itu.
Cœur

1

Saya menemukan masalah di beberapa solusi yang diposting. Saya mengumpulkan ini berdasarkan jawaban di atas. Sepertinya itu berhasil untuk saya.

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

Beberapa solusi yang paling banyak dipilih menyarankan penentu %gkonversi printf. Ini salah karena ada kasus dimana%g notasi ilmiah akan dihasilkan. Solusi lain menggunakan matematika untuk mencetak jumlah digit desimal yang diinginkan.

Saya pikir solusi termudah adalah dengan menggunakan sprintfdengan %fspecifier konversi dan untuk menghapus trailing nol dan mungkin titik desimal dari hasil secara manual. Berikut solusi 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;
}

Perhatikan bahwa karakter yang digunakan untuk angka dan pemisah desimal bergantung pada lokal saat ini. Kode di atas mengasumsikan bahasa Inggris C atau AS.


−0,00005 dan 3 desimal akan menghasilkan "−0" yang mungkin diinginkan atau tidak diinginkan. Juga, mungkin membuat parameter "3" untuk kenyamanan orang lain?
lumba

0

Inilah percobaan pertama saya pada sebuah jawaban:

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

  sprintf (s, format, f);
  untuk (p = s; * p; ++ p)
    jika ('.' == * p) {
      sementara (* ++ p);
      sementara ('0' == * - p) * p = '\ 0';
    }
  printf ("% s", s);
}

Bug yang diketahui: Kemungkinan buffer overflow bergantung pada format. Jika "." ada karena alasan lain selain% f hasil yang salah mungkin terjadi.


Bug Anda yang diketahui sama dengan yang dimiliki printf () itu sendiri, jadi tidak ada masalah di sana. Tetapi saya mencari string format yang memungkinkan saya melakukan apa yang saya inginkan, bukan solusi terprogram, yang sudah saya miliki.
Gorpik

0

Mengapa tidak melakukan ini saja?

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

0

Sedikit variasi di atas:

  1. Menghilangkan periode untuk kasus (10000.0).
  2. Istirahat setelah periode pertama diproses.

Kode di sini:

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

Masih berpotensi meluap, jadi hati-hati; P


0

Saya akan mengatakan Anda harus menggunakan printf("%.8g",value);

Jika Anda menggunakan "%.6g"Anda tidak akan mendapatkan hasil yang diinginkan untuk beberapa nomor seperti 32.230210 itu harus mencetak 32.23021tetapi mencetak32.2302


-2

Kode Anda dibulatkan menjadi tiga tempat desimal karena ".3" sebelum f

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

Jadi jika baris kedua dibulatkan menjadi dua tempat desimal, Anda harus mengubahnya menjadi ini:

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

Kode itu akan menampilkan hasil yang Anda inginkan:

359.013
359.01

* Perhatikan ini dengan asumsi Anda sudah mencetaknya pada baris terpisah, jika tidak maka hal berikut akan mencegahnya mencetak pada baris yang sama:

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

Kode sumber program berikut adalah tes saya untuk jawaban ini

#include <cstdio>

int main()
{

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

    while (true){}

    return 0;

}

4
Pertanyaannya adalah tentang membuat satu format string yang menghilangkan nol di belakangnya, bukan untuk memiliki string format yang berbeda untuk setiap kasus.
Christoph Walesch
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.