Округление double

Тема в разделе "WASM.BEGINNERS", создана пользователем Pahan, 12 дек 2010.

  1. Pahan

    Pahan New Member

    Публикаций:
    0
    Регистрация:
    10 янв 2009
    Сообщения:
    55
    Добрый день! Округление double достаточно широко обсуждается в сети, но тем не менее не смог найти объяснение результатам:

    Код (Text):
    1. int main()
    2. {
    3.  double a=4.975;
    4.  double b;
    5.            
    6. b=round(a*100); //ROUNDING          
    7. cout <<b<<endl;
    8. return 1;
    9. }
    выводит на экран 497, в то время как
    Код (Text):
    1. round(497.5)
    вернет 498

    Понятно, что double не хранит точного значения, но все равно непонятно как такое получается.
    Спасибо, если кто натолкнет на мысль.
     
  2. wsd

    wsd New Member

    Публикаций:
    0
    Регистрация:
    8 авг 2007
    Сообщения:
    2.824
  3. Pahan

    Pahan New Member

    Публикаций:
    0
    Регистрация:
    10 янв 2009
    Сообщения:
    55
    wsd
    -
    ?
     
  4. wsd

    wsd New Member

    Публикаций:
    0
    Регистрация:
    8 авг 2007
    Сообщения:
    2.824
    Pahan
    del my post
     
  5. l_inc

    l_inc New Member

    Публикаций:
    0
    Регистрация:
    29 сен 2005
    Сообщения:
    2.566
    Pahan
    Что значит "непонятно". Как раз исходя из этого и понятно.
    4,975 не является суммой целых степеней двойки, следовательно непредставимо в виде конечной двоичной дроби. Соответственно реально в a будет храниться не 4,975, а 4,974999... . Согласно правилам округления 497,4999... округляется до 497.
    А вот 497,5 является суммой целых степеней двойки: 2^8+2^7+2^6+2^5+2^4+2^0+2^-1. Поэтому будет храниться в виде точного значения. Согласно правилам округления должно быть округлено до 498.
     
  6. Pahan

    Pahan New Member

    Публикаций:
    0
    Регистрация:
    10 янв 2009
    Сообщения:
    55
    l_inc
    Не только на мысль натолкнули, но и ответили подробно. Спасибо.

    Этот вопрос возник в результате поиска функции/алгоритма округления double до n-ого знака после запятой.
    Но так и не получилось найти/придумать что-то чтобы double округлялся по мат. правилам и для положительных и для отрицательных.
    Код c форума cplusplus.com через потоки не заработал. Все найденные мат. алгоритмы падают на отрицательных числах.
    Могли бы Вы и на этот вопрос подсказать ответ?
     
  7. l_inc

    l_inc New Member

    Публикаций:
    0
    Регистрация:
    29 сен 2005
    Сообщения:
    2.566
    Pahan
    Округление именно double до n-го именно десятичного знака после запятой возможно только с определённой точностью. Если Вы допускаете ошибку, например, 10^-10 для чисел в переделах +-10^10 (только пример, т.к. лень считать интервалы для double), то вполне сойдёт способ с умножением на 10^n и последующим округлением. Тогда то же 4,975+-10^-10 вполне правомерно может быть округлено до 2 знака с результатом 4,98 равно как и с результатом 4,97.
    Если же нет, то Вам придётся отказаться от double и использовать собственный формат хранения десятичных дробей. Удобнее всего, наверное, будет использовать bcd с фиксированной запятой: массив десятичных цифр (по байту на каждую, где начиная, например, с десятого байта идёт дробная часть) плюс переменную, хранящую знак.
     
  8. Pahan

    Pahan New Member

    Публикаций:
    0
    Регистрация:
    10 янв 2009
    Сообщения:
    55
    l_inc
    С точностью double все более менее ясно и когда нужна повышенная точность я предпочитаю использовать библиотеку gmp и ее встроенные типы.
    Конечно, можно сказать, что в данной задаче особая точность не нужна, но тем не менее интересно округлить число 4.975 до 2-го знака и получить 4.98. Ведь именно это число (ввожу с клавиатуры) я пытаюсь обработать и хочется получить результат совпадающий с округлением на листочке.
    Пока на листочке написал код:

    Код (Text):
    1. double roundto(double num, int to)
    2. {
    3.  EPS=10e-7; // выбираем какую-то погрешность, подходящую для нашего класса задач
    4.  if (num>=0)
    5.    {
    6.     int n = floor(num*pow(10,to)); // выделяем из числа часть нужного порядка
    7.     double dlt = num*pow(10,to+1) - n*10; // получить что-то типа разницы чисел в  нужном порядке.
    8.                                                          //Например, для num=4.975 dlt= 4.999999998..
    9.    if ((fabs(dlt-5)<2*EPS ) || (dlt>5))// dlt >=5
    10.        n++;
    11.     res=n/pow(10,to);
    12.   }
    13.    else
    14.   {
    15.       int n = ceil(num*pow(10,to)); // выделяем из числа часть нужного порядка
    16.     double dlt = num*pow(10,to+1) - n*10; //
    17.    dlt=-dlt; //делаем разницу положительной. Дальше аналогично
    18.    if ((fabs(dlt-5)<2*EPS ) || (dlt>5))// dlt >=5
    19.        n--;
    20.     res=n/pow(10,to);
    21.  
    22.    }
    23. }
    Могли бы Вы выразить свое мнение по поводу его валидности, пожалуйста?
     
  9. Y_Mur

    Y_Mur Active Member

    Публикаций:
    0
    Регистрация:
    6 сен 2006
    Сообщения:
    2.494
    Pahan
    Я недавно тоже осмысливал похожий вопрос - числа оканчивающиеся на цифру 5 - особый случай "неустойчивого равновесия" когда микроошибка перекинет округление в ту или другую сторону, а как верно заметил l_inc погрешность преобразования между двоичной и десятичной системой в общем случае неизбежна.
     
  10. cppasm

    cppasm New Member

    Публикаций:
    0
    Регистрация:
    18 июл 2006
    Сообщения:
    923
    Микроошибка это не основная причина.
    Показываю фокус, угадайте что выведет (если что, 4.5 и 7.5 представимы абсолютно точно):

    Код (Text):
    1. #include<stdio.h>
    2.  
    3. int main(void)
    4. {
    5.     int n1, n2;
    6.     float x1=4.5f, x2=7.5f;
    7.  
    8.     _asm{
    9.             fld     dword ptr [x1]
    10.             fld     dword ptr [x2]
    11.             frndint
    12.             fistp   dword ptr [n2]
    13.             frndint
    14.             fistp   dword ptr [n1]
    15.     }
    16.  
    17.     printf("N1=%d, N2=%d.\n", n1, n2);
    18.     return 0;
    19. }
    Просто по умолчанию в FPU (и не только) если дробная часть 0.5 - чётные округляются вниз, а нечётные вверх.
     
  11. leo

    leo Active Member

    Публикаций:
    0
    Регистрация:
    4 авг 2004
    Сообщения:
    2.542
    Адрес:
    Russia
    Pahan
    Не понятно, зачем придавать "особый статус" именно значению dlt=5. Если ты задаешь некое значение погрешности EPS, полагая, что все различия <= EPS обусловлены не суперточностью ввода числа юзером, а погрешностями преобразования, то достаточно просто добавить это EPS в стандартный (не "банковский") алгоритм округления:
    Код (Text):
    1.   double d = Pow(10, to);
    2.   if (num >=0)
    3.     { return floor(num*d+0.5+EPS)/d; }
    4.   else
    5.     { return ceil(num*d-0.5-EPS)/d; }
     
  12. Pahan

    Pahan New Member

    Публикаций:
    0
    Регистрация:
    10 янв 2009
    Сообщения:
    55
    leo
    Меня в этом способе интересует следующее:
    Пусть есть число a равное 0.7 и пусть оно хранится как меньше a+EPS. Например: 0.7000...0001 (как верхняя граница возможной записи числа. Может 0.7 и хранится как 0.6999999999..., неважно, главное что принципиально такая ситуация возможна). Тогда 0.700000001 + EPS = 0.7+2EPS - а это уже будет следующее "различимое" число.
    Тогда получается, что при подборе EPS нужно считать не что
    обусловлены погрешностью хранения чисел.
    , а
    все различия <= 2*EPS обусловлены погрешностью хранения чисел.
     
  13. leo

    leo Active Member

    Публикаций:
    0
    Регистрация:
    4 авг 2004
    Сообщения:
    2.542
    Адрес:
    Russia
    1*EPS или 2*EPS - это все "казуистика", т.к. в данном сл.можно исходить только из того, с каким макс.числом дробных знаков могут быть у тебя входные числа и соотв-но брать значение EPS с запасом, чтобы не ломать голову над 1*EPS или 2*EPS.
    Ну а если ты ничего не можешь сказать о макс.точности вх.чисел (т.е. если она может быть сравнима с точностью double), то и смысла во всех этих "хитроумных" округлениях - нет.
     
  14. l_inc

    l_inc New Member

    Публикаций:
    0
    Регистрация:
    29 сен 2005
    Сообщения:
    2.566
    cppasm
    Ну знаете ли фокусы FPU — это вообще не причина. Есть стандарт языка. И его абсолютно не заботит, что там за режим округления у какого-то x87 FPU.
     
  15. leo

    leo Active Member

    Публикаций:
    0
    Регистрация:
    4 авг 2004
    Сообщения:
    2.542
    Адрес:
    Russia
    l_inc
    А в стандарте C\C++ разве есть round ? Вроде только ceil и floor, которые трактуются однозначно, а round может потому и нет, что неизвестно, чьи "фокусы" лучше - банковские\IEEEшные\FPUшные или школьные\арифметические ;)
     
  16. l_inc

    l_inc New Member

    Публикаций:
    0
    Регистрация:
    29 сен 2005
    Сообщения:
    2.566
    leo
    round есть в стандарте. Начиная с C99:
    Похоже, банковские фокусы ISO показались таки хуже. :)
     
  17. leo

    leo Active Member

    Публикаций:
    0
    Регистрация:
    4 авг 2004
    Сообщения:
    2.542
    Адрес:
    Russia
    Мда, странно - значит в "крутом" С\С++ утвердили "школьные" правила округления, а в "школьных" дельфи\паскале, VB и т.п. наоборот - "крутые" банковские ?! :lol:
     
  18. cppasm

    cppasm New Member

    Публикаций:
    0
    Регистрация:
    18 июл 2006
    Сообщения:
    923
    Его не заботит, если использовать round() из C99, для которого чётко сказано что округление идёт от нуля.
    Тут же самописный round(), и приколы FPU очень даже могут влиять.