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

Ответ 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, которых должно быть достаточно для большинства приложений реального мира.