Java двойное сравнение epsilon
Я написал класс, который проверяет равенство, меньше и больше, чем с двумя двойными в 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;
}
Ответы
Ответ 1
Вы НЕ используете double для представления денег. Никогда не. Вместо этого используйте java.math.BigDecimal
.
Затем вы можете указать, как именно делать округление (которое иногда продиктовано законом в финансовых приложениях!) и не нужно делать глупые хаки, подобные этой эпсилонской вещи.
Серьезно, использование типов с плавающей точкой для представления денег крайне непрофессионально.
Ответ 2
Да. 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 выглядят настолько похожими при взгляде на код, я думаю, что они имеют одинаковое значение.
Ответ 3
Whoa whoa whoa. Есть ли конкретная причина, по которой вы используете с плавающей запятой для валюты, или что-то будет лучше с форматом чисел произвольной точности, с фиксированной запятой? Я понятия не имею, какова конкретная проблема, которую вы пытаетесь решить, но вы должны подумать о том, действительно ли или нет полцента, с которым вы хотите работать, или если это просто артефакт использования неточного формата чисел.
Ответ 4
Если вы имеете дело с деньгами, я предлагаю проверить шаблон дизайна Money (первоначально из книга Мартина Фаулера по архитектурному проекту предприятия).
Я предлагаю прочитать эту ссылку для мотивации:
http://wiki.moredesignpatterns.com/space/Value+Object+Motivation+v2
Ответ 5
Если вы можете использовать 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);
}
Ответ 6
Хотя я согласен с идеей о том, что двойное плохо для денег, все же идея сравнения двойников имеет интерес. В частности, предлагаемое использование epsilon подходит только для чисел в определенном диапазоне. Здесь более общее использование epsilon относительно отношения двух чисел (тест для 0 опущено):
boolean equal (double d1, double d2) { double d = d1/d2; return (Math.abs(d - 1,0) < 0,001);
}
Ответ 7
Числа с плавающей запятой содержат только столько значащих цифр, но они могут значительно превышать. Если ваше приложение будет обрабатывать большие числа, вы заметите, что значение 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. Это не хорошее представление о деньгах, если только вы не чертовски уверены, что никогда не будете обрабатывать более миллиона долларов в этой системе.
Ответ 8
Cents? Если вы вычисляете денежные значения, вы действительно не должны использовать значения float. Деньги на самом деле являются счетными значениями. Центы или пенни и т.д. Могут считаться двумя (или любыми) наименее значащими цифрами целого числа. Вы можете хранить и вычислять денежные значения в виде целых чисел и делить на 100 (например, поставить точку или запятую два перед двумя последними цифрами). Использование float может привести к странным ошибкам округления...
В любом случае, если ваш эпсилон должен определять точность, он выглядит слишком маленьким (слишком точным)...
Ответ 9
Как правильно отметили другие комментаторы, вы никогда не должны использовать арифметику с плавающей запятой, когда требуются точные значения, например, для денежных значений. Основная причина - это поведение округления, присущее плавающим точкам, но не забывайте, что дело с плавающими точками означает также иметь дело с бесконечными и значениями 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 Edition, который показывает, что этот подход имеет схожие недостатки и что на самом деле нет надежного приближенного сравнения с плавающей запятой, которое работает для всех диапазонов чисел с плавающей запятой.
Чтобы сделать вещи короткими: воздерживайтесь от double
для денежных значений и используйте точные числа, например BigDecimal
. Для эффективности вы также можете использовать longs
, интерпретируемый как "миллис" (десятые доли центов), если вы надежно предотвращаете переполнение и недополнение. Это дает максимальные представляемые значения 9'223'372'036'854'775.807
, которых должно быть достаточно для большинства приложений реального мира.