Я написал класс, который проверяет равенство, меньше и больше, чем с двумя двойниками в 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;
}
Помимо других проблем, уменьшите вероятность ошибки кодирования, удалив дублированный код. Первый статический метод становится return equals (a, b, EPSILON);
Если говорить просто о красоте, то a == b ? true : x можно заменить на более красивую и удобную для чтения версию a == b || x.
Для дальнейшего обсуждения денег и использования BigDecimal посмотрите: http://stackoverflow.com/questions/285680/presenting-monet ary-values-in-java Спасибо Локи за интересно читать. Я никогда этого не читал, и было бы здорово лучше понять эту тему.




да. Двойники 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 (поэтому я не изменял ваш счет), потому что использование эпсилона вряд ли является «глупым взломом». Это что-то фундаментальное в научных вычислениях, а не «глупый взлом». Статья Голдберга по этому поводу соглашается с этим.
Серьезно, вы не должны думать, что только потому, что вы делаете что-то именно так, это лучший способ во всех случаях. Работая в четырех разных банках, я никогда не видел торговой системы, в которой использовались бы BigDecimal, и не рекомендую их использовать.
Питер, что бы вы посоветовали вместо денег? Я бы предпочел длинный. короткая комбинация для денежного класса. Однако я крайне сомневаюсь, что буду действовать самостоятельно. Я делал это раньше ... но это не то, что я могу доказать, что работает.
Скорее всего, вам понадобится использовать какой-то «десятичный» класс для обработки фактических переводов денег, ОСОБЕННО для розничных клиентов. Это «бухгалтерское программное обеспечение», но не все «финансовое программное обеспечение» является «бухгалтерским». Торговая система имеет дело с A) прогнозируемыми денежными потоками и их текущей стоимостью, которые являются результатом расчетов модели и использование BigDecimal для хранения этих чисел не имеет смысла и B) с фактическими денежными потоками, которые достаточно велики, чтобы люди не беспокоились о пропущенных грошах . Серьезно.
@PeterLawrey :: Тот факт, что вы никогда не видели, чтобы люди использовали что-то отличное от float или double ... не сразу означает, что эти люди знают, что делают. Использование чисел с плавающей запятой для представления денег определенно неправильно и может привести к проблемам. Я уже работал в инвестиционных банках и ... да, я видел, как производственные системы работают неправильно из-за арифметики с плавающей запятой. Вместо этого вы можете использовать BigDecimal или арифметику с фиксированной запятой. Арифметика с фиксированной точкой в основном использует длинные числа и перемещает точку вправо, скажем, на 6 позиций, в зависимости от необходимой точности.
@RichardGomes Я видел множество систем, которые используют BigDecimal и long, и я бы сказал, что использование double наименее подвержено ошибкам, поскольку вы можете легко увидеть небольшое представление и исправить его, но если у вас есть ошибка с BigDecimal или long, это гораздо труднее заметить.
@PeterLawrey :: Правильный способ решения проблемы - использовать арифметику с фиксированной запятой, как я указал в другом ответе на этот вопрос. Вашу аргументацию можно отнести к разряду «апелляции к популярности». en.wikipedia.org/wiki/Argumentum_ad_populum
@RichardGomes предполагает, что есть один правильный ответ, но это не соответствует действительности. Вы можете получить ошибку представления независимо от того, что вы выбрали. Проблема с фиксированной точностью заключается в том, что из-за ошибки кодирования я бы предпочел потерять 0,000000001, а не 10 или более раз.
@PeterLawrey :: Я понимаю вашу точку зрения, и мой ответ более или менее тривиален: хороший набор тестов плюс проверки рисков. В этой отрасли нам нужны системы сигнализации, которые сообщают нам, когда что-то пошло не так. Одним из примеров является то, что Номура предупредил Knight Capital, когда они (Номура) обнаружили, что клиент приносил слишком много убытков подряд в течение 40 минут, что позже было связано с ошибкой программирования. Проблема использования двойников в том, что у вас будут очень маленькие ошибки программирования (ошибки округления), которые очень трудно обнаружить.
@RichardGomes, хотя это правда, очень сложно для ошибки 1 из 1e15 довести до значительного без получения цифр, которые выглядят подозрительно, то есть легко для человеческого пребывания, это выглядит неправильно. Однако, если вы используете long или BigDecimal, у вас, как правило, есть код, который труднее читать / проверять, и вы производите числа, которые человеку гораздо сложнее сказать, это выглядит неправильно.
@Michael Borgwardt: Могу я предложить вам дополнить свой ответ арифметикой с фиксированной точкой? Как BigDecimal, так и арифметика с фиксированной точкой являются рекомендуемыми решениями этого вопроса; второй, когда у вас есть ограничения производительности. Спасибо
Не надо никого оскорблять.
Числа с плавающей запятой имеют ограниченное количество значащих цифр, но они могут быть намного выше. Если ваше приложение когда-либо будет обрабатывать большие числа, вы заметите, что значение 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.
Центов? Если вы рассчитываете денежные значения, вам действительно не следует использовать значения с плавающей запятой. Деньги - это действительно счетные ценности. Центы или пенни и т. д. Можно рассматривать как две (или любые другие) наименее значимые цифры целого числа. Вы можете хранить и вычислять денежные значения как целые числа и делить их на 100 (например, поставить точку или запятую две перед двумя последними цифрами). Использование float может привести к странным ошибкам округления ...
В любом случае, если ваш эпсилон должен определять точность, он выглядит слишком маленьким (слишком точным) ...
Если вы имеете дело с деньгами, я предлагаю проверить шаблон проектирования Money (первоначально из Книга Мартина Фаулера по корпоративному архитектурному дизайну).
Предлагаю прочитать эту ссылку для мотивации: http://wiki.moredesignpatterns.com/space/Value+Object+Motivation+v2
Сервер moredesignpatterns, похоже, ушел и не был заменен. Однако статья есть на archive.org: web.archive.org/web/20090105214308/http://…
Хотя я согласен с идеей, что дабл - это плохо для денег, идея сравнения дабл все же интересна. В частности, предлагаемое использование epsilon подходит только для чисел в определенном диапазоне. Вот более общее использование эпсилона относительно отношения двух чисел (проверка на 0 опущена):
boolean equal(double d1, double d2) {
double d = d1 / d2;
return (Math.abs(d - 1.0) < 0.001);
}
Это очень опасно из-за нулевого деления.
В самом деле, 0.000001 и 0 не были бы равны с этим кодом.
Если вы можете использовать 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))?
Как правильно отметили другие комментаторы, вы должны использовать никогда арифметику с плавающей запятой, когда требуются точные значения, например, для денежных значений. Основная причина действительно заключается в округлении, присущем плавающим точкам, но давайте не будем забывать, что работа с плавающими точками означает также необходимость иметь дело с бесконечными и 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, которых должно хватить для большинства реальных приложений.
Вы пробудили здесь гнев некоторых людей! См. Здесь, если вы действительно хотите использовать числа с плавающей запятой: docs.sun.com/source/806-3568/ncg_goldberg.html