Java двойное сравнение эпсилон - PullRequest
40 голосов
/ 10 декабря 2008

Я написал класс, который проверяет равенство, меньше и больше, чем с двумя двойными числами в 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;
}

Ответы [ 9 ]

101 голосов
/ 10 декабря 2008

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

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

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

10 голосов
/ 10 декабря 2008

Да. Двойники 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 выглядят очень похожими при взгляде на код, я думаю, что они имеют одинаковое значение.

8 голосов
/ 10 декабря 2008

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

6 голосов
/ 04 февраля 2014

Если вы можете использовать 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);
}
5 голосов
/ 10 декабря 2008

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

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

2 голосов
/ 21 октября 2010

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

boolean equal(double d1, double d2) {
  double d = d1 / d2;
  return (Math.abs(d - 1.0) < 0.001);
}
1 голос
/ 10 декабря 2008

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

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

1 голос
/ 10 декабря 2008

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

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

0 голосов
/ 26 февраля 2015

Как правильно заметили другие комментаторы, вы никогда не должны использовать арифметику с плавающей точкой, когда требуются точные значения, например, для денежных значений. Главной причиной действительно является поведение округления, присущее плавающим точкам, но давайте не будем забывать, что работа с плавающими точками означает также необходимость иметь дело с бесконечными значениями и значениями 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 s для денежных значений и используйте точные числовые представления, такие как BigDecimal. Ради эффективности вы также можете использовать longs, интерпретируемый как «миллис» (десятые доли цента), при условии, что вы надежно предотвращаете переполнение и переполнение. Это дает максимально представимые значения 9'223'372'036'854'775.807, которых должно быть достаточно для большинства реальных приложений.

...