Эпсилон двойного сравнения Java

Я написал класс, который проверяет равенство, меньше и больше, чем с двумя двойниками в Java. Мой общий случай - это сравнение цен с точностью до полцента. 59,005 по сравнению с 59,395. Подходит ли выбранный мной эпсилон для этих случаев?

private final static double EPSILON = 0.00001;


/**
 * Returns true if two doubles are considered equal.  Tests if the absolute
 * difference between two doubles has a difference less then .00001.   This
 * should be fine when comparing prices, because prices have a precision of
 * .001.
 *
 * @param a double to compare.
 * @param b double to compare.
 * @return true true if two doubles are considered equal.
 */
public static boolean equals(double a, double b){
    return a == b ? true : Math.abs(a - b) < EPSILON;
}


/**
 * Returns true if two doubles are considered equal. Tests if the absolute
 * difference between the two doubles has a difference less then a given
 * double (epsilon). Determining the given epsilon is highly dependant on the
 * precision of the doubles that are being compared.
 *
 * @param a double to compare.
 * @param b double to compare
 * @param epsilon double which is compared to the absolute difference of two
 * doubles to determine if they are equal.
 * @return true if a is considered equal to b.
 */
public static boolean equals(double a, double b, double epsilon){
    return a == b ? true : Math.abs(a - b) < epsilon;
}


/**
 * Returns true if the first double is considered greater than the second
 * double.  Test if the difference of first minus second is greater then
 * .00001.  This should be fine when comparing prices, because prices have a
 * precision of .001.
 *
 * @param a first double
 * @param b second double
 * @return true if the first double is considered greater than the second
 *              double
 */
public static boolean greaterThan(double a, double b){
    return greaterThan(a, b, EPSILON);
}


/**
 * Returns true if the first double is considered greater than the second
 * double.  Test if the difference of first minus second is greater then
 * a given double (epsilon).  Determining the given epsilon is highly
 * dependant on the precision of the doubles that are being compared.
 *
 * @param a first double
 * @param b second double
 * @return true if the first double is considered greater than the second
 *              double
 */
public static boolean greaterThan(double a, double b, double epsilon){
    return a - b > epsilon;
}


/**
 * Returns true if the first double is considered less than the second
 * double.  Test if the difference of second minus first is greater then
 * .00001.  This should be fine when comparing prices, because prices have a
 * precision of .001.
 *
 * @param a first double
 * @param b second double
 * @return true if the first double is considered less than the second
 *              double
 */
public static boolean lessThan(double a, double b){
    return lessThan(a, b, EPSILON);
}


/**
 * Returns true if the first double is considered less than the second
 * double.  Test if the difference of second minus first is greater then
 * a given double (epsilon).  Determining the given epsilon is highly
 * dependant on the precision of the doubles that are being compared.
 *
 * @param a first double
 * @param b second double
 * @return true if the first double is considered less than the second
 *              double
 */
public static boolean lessThan(double a, double b, double epsilon){
    return b - a > epsilon;
}

Вы пробудили здесь гнев некоторых людей! См. Здесь, если вы действительно хотите использовать числа с плавающей запятой: docs.sun.com/source/806-3568/ncg_goldberg.html

Loki 10.12.2008 20:31

Помимо других проблем, уменьшите вероятность ошибки кодирования, удалив дублированный код. Первый статический метод становится return equals (a, b, EPSILON);

null 27.04.2013 05:32

Если говорить просто о красоте, то a == b ? true : x можно заменить на более красивую и удобную для чтения версию a == b || x.

Matthias 02.04.2014 20:13

Для дальнейшего обсуждения денег и использования BigDecimal посмотрите: http://stackoverflow.com/questions/285680/presenting-monet‌ ary-values-in-java Спасибо Локи за интересно читать. Я никогда этого не читал, и было бы здорово лучше понять эту тему.

dshaw 10.12.2008 21:23
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
52
4
102 310
9

Ответы 9

да. Двойники Java сохранят свою точность лучше, чем ваш заданный эпсилон 0,00001.

Любая ошибка округления, возникающая из-за хранения значений с плавающей запятой, будет меньше 0,00001. Я регулярно использую 1E-6 или 0,000001 для двойного эпсилона в Java без проблем.

Кстати, мне нравится формат epsilon = 1E-5;, потому что я считаю его более читаемым (1E-5 в Java = 1 x 10 ^ -5). 1E-6 легко отличить от 1E-5 при чтении кода, тогда как 0,00001 и 0,000001 выглядят очень похожими при взгляде на код, я думаю, что это одно и то же значение.

Эй, эй, эй. Есть ли конкретная причина, по которой вы используете числа с плавающей запятой для валюты, или было бы лучше с формат чисел с фиксированной запятой произвольной точности? Я понятия не имею, в чем конкретная проблема, которую вы пытаетесь решить, но вы должны подумать, действительно ли полцента - это то, с чем вы хотите работать, или это просто артефакт использования неточного числового формата.

Вы НЕ используете двойное значение для обозначения денег. Никогда не. Вместо этого используйте java.math.BigDecimal.

Затем вы можете указать, как именно выполнять округление (что иногда диктуется законом в финансовых приложениях!), И вам не нужно делать глупые взломы, подобные этой штуке с эпсилоном.

Серьезно, использование типов с плавающей запятой для представления денег крайне непрофессионально.

+1, потому что на самом деле вы никогда не используете числа с плавающей запятой для обозначения денег, кроме -1 (поэтому я не изменял ваш счет), потому что использование эпсилона вряд ли является «глупым взломом». Это что-то фундаментальное в научных вычислениях, а не «глупый взлом». Статья Голдберга по этому поводу соглашается с этим.

SyntaxT3rr0r 04.03.2010 16:46

Серьезно, вы не должны думать, что только потому, что вы делаете что-то именно так, это лучший способ во всех случаях. Работая в четырех разных банках, я никогда не видел торговой системы, в которой использовались бы BigDecimal, и не рекомендую их использовать.

Peter Lawrey 21.10.2010 23:35

Питер, что бы вы посоветовали вместо денег? Я бы предпочел длинный. короткая комбинация для денежного класса. Однако я крайне сомневаюсь, что буду действовать самостоятельно. Я делал это раньше ... но это не то, что я могу доказать, что работает.

monksy 22.01.2013 08:13

Скорее всего, вам понадобится использовать какой-то «десятичный» класс для обработки фактических переводов денег, ОСОБЕННО для розничных клиентов. Это «бухгалтерское программное обеспечение», но не все «финансовое программное обеспечение» является «бухгалтерским». Торговая система имеет дело с A) прогнозируемыми денежными потоками и их текущей стоимостью, которые являются результатом расчетов модели и использование BigDecimal для хранения этих чисел не имеет смысла и B) с фактическими денежными потоками, которые достаточно велики, чтобы люди не беспокоились о пропущенных грошах . Серьезно.

quant_dev 30.04.2013 12:23

@PeterLawrey :: Тот факт, что вы никогда не видели, чтобы люди использовали что-то отличное от float или double ... не сразу означает, что эти люди знают, что делают. Использование чисел с плавающей запятой для представления денег определенно неправильно и может привести к проблемам. Я уже работал в инвестиционных банках и ... да, я видел, как производственные системы работают неправильно из-за арифметики с плавающей запятой. Вместо этого вы можете использовать BigDecimal или арифметику с фиксированной запятой. Арифметика с фиксированной точкой в ​​основном использует длинные числа и перемещает точку вправо, скажем, на 6 позиций, в зависимости от необходимой точности.

Richard Gomes 22.01.2016 19:09

@RichardGomes Я видел множество систем, которые используют BigDecimal и long, и я бы сказал, что использование double наименее подвержено ошибкам, поскольку вы можете легко увидеть небольшое представление и исправить его, но если у вас есть ошибка с BigDecimal или long, это гораздо труднее заметить.

Peter Lawrey 22.01.2016 21:33

@PeterLawrey :: Правильный способ решения проблемы - использовать арифметику с фиксированной запятой, как я указал в другом ответе на этот вопрос. Вашу аргументацию можно отнести к разряду «апелляции к популярности». en.wikipedia.org/wiki/Argumentum_ad_populum

Richard Gomes 24.01.2016 17:19

@RichardGomes предполагает, что есть один правильный ответ, но это не соответствует действительности. Вы можете получить ошибку представления независимо от того, что вы выбрали. Проблема с фиксированной точностью заключается в том, что из-за ошибки кодирования я бы предпочел потерять 0,000000001, а не 10 или более раз.

Peter Lawrey 24.01.2016 17:27

@PeterLawrey :: Я понимаю вашу точку зрения, и мой ответ более или менее тривиален: хороший набор тестов плюс проверки рисков. В этой отрасли нам нужны системы сигнализации, которые сообщают нам, когда что-то пошло не так. Одним из примеров является то, что Номура предупредил Knight Capital, когда они (Номура) обнаружили, что клиент приносил слишком много убытков подряд в течение 40 минут, что позже было связано с ошибкой программирования. Проблема использования двойников в том, что у вас будут очень маленькие ошибки программирования (ошибки округления), которые очень трудно обнаружить.

Richard Gomes 24.02.2016 23:15

@RichardGomes, хотя это правда, очень сложно для ошибки 1 из 1e15 довести до значительного без получения цифр, которые выглядят подозрительно, то есть легко для человеческого пребывания, это выглядит неправильно. Однако, если вы используете long или BigDecimal, у вас, как правило, есть код, который труднее читать / проверять, и вы производите числа, которые человеку гораздо сложнее сказать, это выглядит неправильно.

Peter Lawrey 25.02.2016 11:20

@Michael Borgwardt: Могу я предложить вам дополнить свой ответ арифметикой с фиксированной точкой? Как BigDecimal, так и арифметика с фиксированной точкой являются рекомендуемыми решениями этого вопроса; второй, когда у вас есть ограничения производительности. Спасибо

Richard Gomes 11.05.2017 20:57

Не надо никого оскорблять.

Cupitor 21.07.2017 20:43

Числа с плавающей запятой имеют ограниченное количество значащих цифр, но они могут быть намного выше. Если ваше приложение когда-либо будет обрабатывать большие числа, вы заметите, что значение epsilon должно быть другим.

0,001 + 0,001 = 0,002 НО 12 345 678 900 000 000 000 000 + 1 = 12 345 678 900 000 000 000 000 если вы используете числа с плавающей запятой и double. Это не очень хорошее представление о деньгах, если только вы не уверены, что никогда не потратите больше миллиона долларов в этой системе.

Плавающая точка не представляет точно такие значения, как 0,1, поскольку внутри она хранит значение как 2 ^ exponent * (1 + дробь). Даже в разумных пределах, например 0,001 + 0,001. Запустите "print int (1.13 * 100.0) / 100.0", если у вас есть perl. Возвращает 1.12.

Eugene Yokota 10.12.2008 20:45

Центов? Если вы рассчитываете денежные значения, вам действительно не следует использовать значения с плавающей запятой. Деньги - это действительно счетные ценности. Центы или пенни и т. д. Можно рассматривать как две (или любые другие) наименее значимые цифры целого числа. Вы можете хранить и вычислять денежные значения как целые числа и делить их на 100 (например, поставить точку или запятую две перед двумя последними цифрами). Использование float может привести к странным ошибкам округления ...

В любом случае, если ваш эпсилон должен определять точность, он выглядит слишком маленьким (слишком точным) ...

Если вы имеете дело с деньгами, я предлагаю проверить шаблон проектирования Money (первоначально из Книга Мартина Фаулера по корпоративному архитектурному дизайну).

Предлагаю прочитать эту ссылку для мотивации: http://wiki.moredesignpatterns.com/space/Value+Object+Motivation+v2

Сервер moredesignpatterns, похоже, ушел и не был заменен. Однако статья есть на archive.org: web.archive.org/web/20090105214308/http://…

Joshua Goldberg 03.06.2013 23:55

Хотя я согласен с идеей, что дабл - это плохо для денег, идея сравнения дабл все же интересна. В частности, предлагаемое использование epsilon подходит только для чисел в определенном диапазоне. Вот более общее использование эпсилона относительно отношения двух чисел (проверка на 0 опущена):

boolean equal(double d1, double d2) {
  double d = d1 / d2;
  return (Math.abs(d - 1.0) < 0.001);
}

Это очень опасно из-за нулевого деления.

lethalman 18.12.2011 14:18

В самом деле, 0.000001 и 0 не были бы равны с этим кодом.

Joey 09.07.2012 12:02

Если вы можете использовать BigDecimal, используйте его, иначе:

/**
  *@param precision number of decimal digits
  */
public static boolean areEqualDouble(double a, double b, int precision) {
   return Math.abs(a - b) <= Math.pow(10, -precision);
}

Разве это не должно быть Double.compare (Math.abs (a-b), Math.pow (10, -precision))?

Michael 19.07.2014 00:19

Как правильно отметили другие комментаторы, вы должны использовать никогда арифметику с плавающей запятой, когда требуются точные значения, например, для денежных значений. Основная причина действительно заключается в округлении, присущем плавающим точкам, но давайте не будем забывать, что работа с плавающими точками означает также необходимость иметь дело с бесконечными и NaN значениями.

В качестве иллюстрации того, что ваш подход просто не работает, вот простой тестовый код. Я просто добавляю ваш EPSILON к 10.0 и смотрю, равен ли результат 10.0 - чего не должно быть, поскольку разница явно не меньше, а EPSILON:

    double a = 10.0;
    double b = 10.0 + EPSILON;
    if (!equals(a, b)) {
        System.out.println("OK: " + a + " != " + b);
    } else {
        System.out.println("ERROR: " + a + " == " + b);
    }

Сюрприз:

    ERROR: 10.0 == 10.00001

Ошибки возникают из-за потери значимых битов при вычитании, если два значения с плавающей запятой имеют разные показатели степени.

Если вы думаете о применении более продвинутого подхода «относительной разницы», предложенного другими комментаторами, вам следует прочитать превосходную статью Брюса Доусона Сравнение чисел с плавающей запятой, издание 2012 г., в которой показано, что этот подход имеет аналогичные недостатки и что на самом деле существует отказоустойчивое приближенное сравнение с плавающей запятой нет. который работает для всех диапазонов чисел с плавающей запятой.

Вкратце: воздержитесь от double для денежных значений и используйте точные числовые представления, такие как BigDecimal. Для повышения эффективности вы также можете использовать longs, интерпретируемый как «миллис» (десятые доли центов), если вы надежно предотвращаете переполнение и недостаточное заполнение. Это дает максимально представимые значения 9'223'372'036'854'775.807, которых должно хватить для большинства реальных приложений.

Другие вопросы по теме