Як обмежити значення поплавця лише двома місцями після десяткової крапки в С?


215

Як я можу округлити значення поплавця (наприклад, 37,777779) до двох знаків після коми (37,78) у С?


15
Ви не можете правильно округлити число, оскільки floatdouble) не є десятковою плаваючою комою - вони двійкові з плаваючою комою - тому округлення до десяткових позицій безглуздо. Однак ви можете округлити вихід.
Павло Мінаєв

63
Це не безглуздо; це неточно. Існує велика різниця.
Брукс Мойсей

2
Якого закруглення ви очікуєте? Напів-закруглення або округлення до найближчого парного?
Truthseeker Rangwan

Відповіді:


407

Якщо ви просто хочете округлити число для цілей виведення, то "%.2f"рядки формату справді є правильною відповіддю. Однак якщо ви дійсно хочете округлити значення з плаваючою комою для подальшого обчислення, працює щось на зразок наступних:

#include <math.h>

float val = 37.777779;

float rounded_down = floorf(val * 100) / 100;   /* Result: 37.77 */
float nearest = roundf(val * 100) / 100;  /* Result: 37.78 */
float rounded_up = ceilf(val * 100) / 100;      /* Result: 37.78 */

Зауважте, що ви можете обрати три різні правила округлення: округлення вниз (тобто скорочення після двох знаків після коми), округлене до найближчого та округлене. Зазвичай, ви хочете, щоб найближчий круглий.

Як зазначають декілька інших, через химерність подання з плаваючою точкою ці округлі значення можуть бути не «явними» десятковими значеннями, але вони будуть дуже близькими.

Більше (набагато!) Більше інформації про округлення, і особливо про правила розбиття краватки до найближчого, дивіться у статті Вікіпедії про округлення .


4
Чи можна його модифікувати для підтримки округлення до довільної точності?

1
@slater Коли ви говорите "довільна точність", ви запитуєте про округлення до, наприклад, трьох, а не двох знаків після коми, або використовуєте бібліотеки, які реалізують десятичні значення без обмеженої точності? Якщо перший, то, я сподіваюся, внесіть очевидні корективи в постійну 100; в іншому випадку зробіть такі самі розрахунки, як показано вище, точно з будь-якою багатоточною бібліотекою, яку ви використовуєте.
Дейл Хагглунд

2
@DaleHagglung Колишній, дякую. Чи потрібне регулювання для заміни 100 на pow (10, (int) бажаної точності)?

3
Так. Для округлення після k десяткових знаків використовуйте масштабний коефіцієнт 10 ^ k. Це має бути дуже легко зрозуміти, якщо ви виписуєте вручну кілька десяткових значень і граєте з кратними 10. Припустимо, ви працюєте зі значенням 1,223456789 і хочете округлити його до 3 знаків після коми. Доступна для вас операція кругла до цілого . Отже, як перемістити перші три десяткових знаки, щоб вони залишилися від десяткової крапки? Сподіваюся, зрозуміло, що ви помножите на 10 ^ 3. Тепер ви можете округлити це значення до цілого числа. Далі ви ставите три цифри низького порядку назад, діливши на 10 ^ 3.
Дейл Хагглунд

1
Чи можу я зробити цю роботу doublesзанадто якось? Здається, не виконую роботу, яку я хочу :( (використовую floorта ceil).
Пані Ніхто

87

Використання % .2f у printf. Він друкує лише 2 десяткові крапки.

Приклад:

printf("%.2f", 37.777779);

Вихід:

37.77

Цей спосіб кращий, тому що немає втрати точності.
Альберт

2
@albert Це також має перевагу: відсутність втрати floatдальності, оскільки це val * 100може переповнитися.
chux

42

Якщо припустити, що ви говорите про кругле значення для друку, то відповідь Ендрю Колсона та AraK правильна:

printf("%.2f", 37.777779);

Але зауважте, що якщо ви маєте на меті округлити число до 37,78 для внутрішнього використання (наприклад, для порівняння з іншим значенням), це не є хорошою ідеєю, через те, як працюють номери з плаваючою комою: зазвичай ви не робите Ви хочете зробити порівняння рівності для плаваючої точки, замість цього використовуйте цільове значення +/- значення сигми. Або кодуйте число як рядок з відомою точністю і порівняйте це.

Дивіться посилання у відповіді Грега Хьюгілла на пов'язане питання , яке також висвітлює, чому не слід використовувати плаваючу крапку для фінансових розрахунків.


1
Запропоновано вирішити питання, що може бути питанням, яке стоїть за питанням (або питанням, яке повинно було стояти за цим питанням!) Це досить важливий момент.
Брукс Мойсей

Насправді 37,78 можна представити саме плаваючою точкою. Float має від 11 до 12 цифр для точності. Цього має бути достатньо для адреси 3778 377,8 або всього 4-х значного числа після коми.
Анонімний Білий

@HaryantoCiu так справедливо, я трохи відредагував свою відповідь.
Джон Картер

динамічна точність:printf("%.*f", (int)precision, (double)number);
Minhas Kamal

24

Як щодо цього:

float value = 37.777779;
float rounded = ((int)(value * 100 + .5) / 100.0);

4
-1: а) це не буде працювати для від'ємних чисел (нормально, приклад позитивний, але все-таки). б) ви не згадуєте, що неможливо зберегти точне десяткове значення у поплавці
Джон Картер,

32
@therefromhere: (a) Ти правий (b) Що це? Тест середньої школи?
Даніїл

1
чому ви додали 0,5?
Мухаммад тайяб

1
Необхідно дотримуватися правил округлення.
Даніїл

1
правила округлення в контексті коментаря @Daniil є близькими до найближчого
Shmil The Cat

20
printf("%.2f", 37.777779);

Якщо ви хочете записати на C-рядок:

char number[24]; // dummy size, you should take care of the size!
sprintf(number, "%.2f", 37.777779);

@Sinan: Навіщо редагувати? @AraK: Ні, ви повинні подбати про розмір :). Використовуйте snprintf ().
аїб

1
@aib: Я б здогадався, тому що / ** / є коментарями у стилі C, а питання позначене темою C
Майкл Харен

5
C89 дозволений / ** / - стиль, C99 представив підтримку // - стилю. Використовуйте кульгавий / старий компілятор (або форсуйте режим C89), і ви не зможете використовувати // - style. Сказавши це, це 2009 рік, давайте розглянемо їх як стиль C, так і C ++.
Ендрю Коулсон

11

Не існує способу округлення а floatдо іншої, floatтому що округлене floatможе не бути представним (обмеження чисел з плаваючою комою). Наприклад, скажімо, що ви обійшли від 37,777779 до 37,78, але найближче представницьке число - 37,781.

Однак ви можете "округлити" a floatза допомогою функції рядка формату.


3
Це не відрізняється від того, що сказати "немає способу розділити два поплавця і отримати поплавок, тому що розділений результат може бути непредставитим", що може бути точно правдою, але не має значення. Поплавці завжди неточні, навіть для чогось такого основного, як додавання; припущення завжди полягає в тому, що те, що ви насправді отримуєте, - це "поплавок, який найбільш наближає точну округлу відповідь".
Брукс Мойсей

Я мав на увазі те, що ви не можете округлити від a floatдо n десяткових знаків і тоді очікуєте, що результат завжди матиме n десятичних знаків. Ви все одно отримаєте float, тільки не той, якого ви очікували.
Ендрю Кітон

9

Крім того, якщо ви використовуєте C ++, ви можете просто створити таку функцію:

string prd(const double x, const int decDigits) {
    stringstream ss;
    ss << fixed;
    ss.precision(decDigits); // set # places after decimal
    ss << x;
    return ss.str();
}

Потім ви можете вивести будь-який подвійний myDoubleз nмісцями після десяткової крапки з таким кодом:

std::cout << prd(myDouble,n);

7

Ви все ще можете використовувати:

float ceilf(float x); // don't forget #include <math.h> and link with -lm.

приклад:

float valueToRound = 37.777779;
float roundedValue = ceilf(valueToRound * 100) / 100;

Це відсікається в десятковій точці (тобто вийде 37), і йому потрібно округлювати до двох місць після десяткової крапки.
Павло Мінаєв

Округлення до двох місць після десяткової крапки є тривіальною варіацією, але (але все ж слід згадати у відповіді; ZeroCool, хочете додати редагування?): Float roundedValue = ceilf (valueToRound * 100.0) / 100.0;
Брукс Мойсей

У стан сну :)
ZeroCool

Чому це рішення не є більш популярним? Це працює саме так, як слід з мінімальним кодом. Чи є з цим якийсь застереження?
Енді

7

У програмі C ++ (або в C із кастами у стилі C) ви можете створити функцію:

/* Function to control # of decimal places to be output for x */
double showDecimals(const double& x, const int& numDecimals) {
    int y=x;
    double z=x-y;
    double m=pow(10,numDecimals);
    double q=z*m;
    double r=round(q);

    return static_cast<double>(y)+(1.0/m)*r;
}

Тоді std::cout << showDecimals(37.777779,2);було б вироблено: 37,78.

Очевидно, вам не потрібно створювати всі 5 змінних у цій функції, але я залишаю їх там, щоб ви могли бачити логіку. Можливо, є більш прості рішення, але це добре працює для мене - тим більше, що це дозволяє мені коригувати кількість цифр після десяткового знака, як мені потрібно.


5

Завжди використовуйте для цього printfсімейство функцій. Навіть якщо ви хочете отримати значення як float, вам найкраще використовувати snprintfокруглене значення як рядок, а потім розбирати його назад з atof:

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

double dround(double val, int dp) {
    int charsNeeded = 1 + snprintf(NULL, 0, "%.*f", dp, val);
    char *buffer = malloc(charsNeeded);
    snprintf(buffer, charsNeeded, "%.*f", dp, val);
    double result = atof(buffer);
    free(buffer);
    return result;
}

Я говорю це тому, що підхід, показаний у відповіді, що наразі відповідає головою, та декілька інших тут - множення на 100, округлення до найближчого цілого числа, а потім повторне ділення на 100 - помилково двома способами:

  • Для деяких значень він закруглятиметься в неправильному напрямку, оскільки множення на 100 змінює десяткову цифру, визначаючи напрямок округлення від 4 до 5 або навпаки, через неточність чисел з плаваючою комою
  • Для деяких значень множення та ділення на 100 не означає, що навіть якщо не відбудеться округлення, кінцевий результат буде помилковим

Щоб проілюструвати помилку першого типу - напрямок округлення, часом невірний - спробуйте запустити цю програму:

int main(void) {
    // This number is EXACTLY representable as a double
    double x = 0.01499999999999999944488848768742172978818416595458984375;

    printf("x: %.50f\n", x);

    double res1 = dround(x, 2);
    double res2 = round(100 * x) / 100;

    printf("Rounded with snprintf: %.50f\n", res1);
    printf("Rounded with round, then divided: %.50f\n", res2);
}

Ви побачите цей вихід:

x: 0.01499999999999999944488848768742172978818416595459
Rounded with snprintf: 0.01000000000000000020816681711721685132943093776703
Rounded with round, then divided: 0.02000000000000000041633363423443370265886187553406

Зауважимо, що значення, з якого ми розпочали, було менше 0,015, і тому математично правильна відповідь при округлянні її до двох десяткових знаків становить 0,01. Звичайно, 0,01 не є точно репрезентативним як подвійний, але ми очікуємо, що наш результат буде подвійним найближчим до 0,01. Використання snprintfдає нам такий результат, але використання round(100 * x) / 100дає нам 0,02, що неправильно. Чому? Тому що 100 * xдає нам рівно 1,5. Помноження на 100, таким чином, змінює правильний напрямок на округлення.

Для ілюстрації другого роду помилки - результат іноді бути неправильно з - за * 100і на / 100самому ділі не бути зворотним один одного - ми можемо зробити подібне вправу з дуже великим числом:

int main(void) {
    double x = 8631192423766613.0;

    printf("x: %.1f\n", x);

    double res1 = dround(x, 2);
    double res2 = round(100 * x) / 100;

    printf("Rounded with snprintf: %.1f\n", res1);
    printf("Rounded with round, then divided: %.1f\n", res2);
}

Наше число зараз навіть не має дробової частини; це ціле значення, просто збережене з типом double. Отже результат після округлення повинен бути тим самим числом, з якого ми почали, правда?

Якщо запустити програму вище, ви побачите:

x: 8631192423766613.0
Rounded with snprintf: 8631192423766613.0
Rounded with round, then divided: 8631192423766612.0

На жаль Наш snprintfметод знову повертає правильний результат, але підхід «множина-тоді-кругообіг» та «ділення» не вдається. Це тому , що математично правильне значення 8631192423766613.0 * 100, 863119242376661300.0є не зовсім представимо в вигляді подвійної; найближче значення 863119242376661248.0. Якщо ви розділите це назад на 100, ви отримаєте 8631192423766612.0- інше число, ніж те, з якого ви почали.

Сподіваємось, це є достатньою демонстрацією того, що використання roundfдля округлення до числа десяткових знаків порушено, і що ви повинні використовувати його snprintfзамість. Якщо це здасться вам жахливим злом, можливо, вас заспокоїть знання, що це в основному те, що робить CPython .


+1 для конкретного прикладу того, що йде не так у моїй відповіді та подібних до неї, завдяки дивності плаваючої точки IEEE та наданню прямої альтернативи. Я досить давно знав багато зусиль, що були вкладені в друк, і друзі мені їх безпечні для круглих значень з плаваючою точкою. Я б здогадався, що зроблена тоді робота може виявлятися тут.
Дейл

Ах ... Вибачте за слово салат біля кінця, яке зараз редагувати вже пізно. Що я хотів сказати: "... багато зусиль доклали до printf та друзів, щоб зробити їх безпечними ..."
Дейл

4

Використовуйте float roundf(float x).

"Функції" круглого "округлюють аргумент до найближчого цілого значення у форматі з плаваючою комою, округлюючи половину випадків від нуля, незалежно від поточного напрямку округлення." C11dr §7.12.9.5

#include <math.h>
float y = roundf(x * 100.0f) / 100.0f; 

Залежно від вашої floatреалізації, цифри, які можуть здатися на півдорозі, не є. як плаваюча точка, як правило, орієнтована на базу-2. Далі, саме округлення до найближчого 0.01у всіх випадках "на півдорозі" є найбільш складним.

void r100(const char *s) {
  float x, y;
  sscanf(s, "%f", &x);
  y = round(x*100.0)/100.0;
  printf("%6s %.12e %.12e\n", s, x, y);
}

int main(void) {
  r100("1.115");
  r100("1.125");
  r100("1.135");
  return 0;
}

 1.115 1.115000009537e+00 1.120000004768e+00  
 1.125 1.125000000000e+00 1.129999995232e+00
 1.135 1.134999990463e+00 1.139999985695e+00

Хоча «1.115» є «півдорозі» між 1.11 і 1.12, при перетворенні в floatзначенні не є 1.115000009537...і більше не « на півдорозі», але ближче до 1.12 і патронам до найближчого floatз1.120000004768...

"1.125" є "на півдорозі" між 1,12 та 1,13, при перетворенні floatв значення є рівно 1.125і є "на півдорозі". Вона округлює в стороні 1,13 з - за зв'язки з навіть правил і округляється до найближчого floatз1.129999995232...

Хоча "1.135" є "на півдорозі" між 1,13 і 1,14, при перетворенні в floatце значення є 1.134999990463...і вже не "на півдорозі", а ближче до 1,13 і округляється до найближчого floatз1.129999995232...

Якщо використовується код

y = roundf(x*100.0f)/100.0f;

Хоча «1,135» є « на півдорозі» між 1.13 і 1.14, при перетворенні в floatзначенні не є 1.134999990463...і більше не « на півдорозі», але ближче до 1,13 , але неправильно округляється до floatз 1.139999985695...з - за більш обмежену точність floatVS. double. Це неправильне значення може розглядатися як правильне, залежно від цілей кодування.


4

Я зробив цей макрос для округлення плаваючих чисел. Додайте його до свого заголовка / буття файлу

#define ROUNDF(f, c) (((float)((int)((f) * (c))) / (c)))

Ось приклад:

float x = ROUNDF(3.141592, 100)

х дорівнює 3,14 :)


Це скорочується, але питання вимагає округлення. Крім того, він піддається помилкам округлення в операціях з плаваючою комою.
Eric Postpischil

3
double f_round(double dval, int n)
{
    char l_fmtp[32], l_buf[64];
    char *p_str;
    sprintf (l_fmtp, "%%.%df", n);
    if (dval>=0)
            sprintf (l_buf, l_fmtp, dval);
    else
            sprintf (l_buf, l_fmtp, dval);
    return ((double)strtod(l_buf, &p_str));

}

Ось nкількість десятків

приклад:

double d = 100.23456;

printf("%f", f_round(d, 4));// result: 100.2346

printf("%f", f_round(d, 2));// result: 100.23

-1 з чотирьох причин: 1) відсутність пояснень; 2) вразливість до переповнення буфера - це переповниться, а отже, цілком можливо, збій, якщо він dvalє величезним 3) дивний if/ elseблок, де ви робите точно те саме в кожній гілці і 4) надскладне використання sprintfдля побудови специфікатора формату для другого sprintfвиклику; простіше просто використовувати .*та передавати подвійне значення та кількість десяткових знаків як аргументи на один і той же sprintfвиклик.
Марк Амерді

3

Визначення коду:

#define roundz(x,d) ((floor(((x)*pow(10,d))+.5))/pow(10,d))

Результати:

a = 8.000000
sqrt(a) = r = 2.828427
roundz(r,2) = 2.830000
roundz(r,3) = 2.828000
roundz(r,5) = 2.828430

0

Дозвольте спершу спробувати виправдати свою причину для того, щоб додати ще одну відповідь на це питання. В ідеальному світі округлення насправді не є великою справою. Однак у реальних системах вам може знадобитися вирішити декілька питань, які можуть призвести до округлення, яке може бути не таким, як ви очікуєте. Наприклад, ви можете виконувати фінансові розрахунки, коли кінцеві результати округляються і відображаються користувачам у вигляді 2 знаків після коми; ці ж значення зберігаються з фіксованою точністю в базі даних, яка може містити більше 2 знаків після коми (з різних причин; немає оптимальної кількості місць для зберігання ... залежить від конкретних ситуацій, які повинна підтримувати кожна система, наприклад, крихітні предмети, ціни яких є частками копійки на одиницю); і, обчислення з плаваючою комою, що виконуються на значеннях, де результати плюс / мінус епсилон. Я стикався з цими питаннями і розробляв власну стратегію протягом багатьох років. Я не буду стверджувати, що зіткнувся з кожним сценарієм або маю найкращу відповідь, але нижче наводиться приклад мого підходу, який подолав ці проблеми:

Припустимо, що 6 десяткових знаків розглядаються як достатня точність для розрахунків на поплавці / парні (довільне рішення для конкретного застосування), використовуючи наступну функцію / метод округлення:

double Round(double x, int p)
{
    if (x != 0.0) {
        return ((floor((fabs(x)*pow(double(10.0),p))+0.5))/pow(double(10.0),p))*(x/fabs(x));
    } else {
        return 0.0;
    }
}

Округлення до 2 знаків після коми для подання результату може бути виконано у вигляді:

double val;
// ...perform calculations on val
String(Round(Round(Round(val,8),6),2));

Бо val = 6.825результат є6.83 як очікувалося.

Бо val = 6.824999результат є 6.82. Тут припущення полягає в тому, що розрахунок привів точно6.824999 і 7-й десятковий знак дорівнює нулю.

Бо val = 6.8249999результат є 6.83. У 9цьому випадку 7-й десятковий знак призводить до того, що Round(val,6)функція дає очікуваний результат. У цьому випадку може бути будь-яка кількість трейлінгу9 s.

Бо val = 6.824999499999результат є 6.83. Округлення до 8-го десяткових знаків як перший крок, тобто Round(val,8)береться за один неприємний випадок, коли обчислений результат з плаваючою комою обчислюється до6.8249995 , але внутрішньо представлений як6.824999499999... .

Нарешті, приклад із питання ... val = 37.777779приводить до37.78 .

Цей підхід можна було б узагальнити як:

double val;
// ...perform calculations on val
String(Round(Round(Round(val,N+2),N),2));

де N - необхідно підтримувати точність для всіх проміжних розрахунків на поплавці / парні. Це працює і на негативних значеннях. Я не знаю, чи такий підхід математично правильний для всіх можливостей.



-1

... або ви можете зробити це старомодно без будь-яких бібліотек:

float a = 37.777779;

int b = a; // b = 37    
float c = a - b; // c = 0.777779   
c *= 100; // c = 77.777863   
int d = c; // d = 77;    
a = b + d / (float)100; // a = 37.770000;

Звичайно, якщо ви хочете видалити зайву інформацію з номера.


-2

ця функція приймає число і точність і повертає округлене число

float roundoff(float num,int precision)
{
      int temp=(int )(num*pow(10,precision));
      int num1=num*pow(10,precision+1);
      temp*=10;
      temp+=5;
      if(num1>=temp)
              num1+=10;
      num1/=10;
      num1*=10;
      num=num1/pow(10,precision+1);
      return num;
}

він перетворює номер плаваючої точки в int шляхом зсуву точки вліво і перевірки наявності на більш ніж п’ять умов.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.