Является ли это допустимым сравнением с плавающей запятой, учитывающим заданное количество знаков после запятой?

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

/// <summary>
/// Determines if the float value is equal to (==) the float parameter according to the defined precision.
/// </summary>
/// <param name="float1">The float1.</param>
/// <param name="float2">The float2.</param>
/// <param name="precision">The precision.  The number of digits after the decimal that will be considered when comparing.</param>
/// <returns></returns>
public static bool AlmostEquals(this float float1, float float2, int precision = 2)
{
    return (Math.Round(float1 - float2, precision) == 0);
}

Примечание. Я ищу сравнение по десятичным разрядам, а не по допуску. Я не хочу, чтобы 1 000 000 равнялось 1 000 001.


person Kim    schedule 07.02.2012    source источник
comment
Я бы написал набор модульных тестов, чтобы проверить, подходит ли ваш алгоритм для ваших нужд.   -  person Wouter de Kort♦    schedule 07.02.2012
comment
Мне нравится это название метода: AlmostEquals...   -  person gdoron is supporting Monica    schedule 07.02.2012
comment
Я написал несколько модульных тестов, и они проходят с предоставленными значениями, но я хотел бы получить совет от аудитории с более глубоким пониманием реализации/поведения float.   -  person Kim    schedule 07.02.2012
comment
Math.Round() выдает исключение, если точность меньше нуля или больше 15.   -  person Igor Korkhov    schedule 07.02.2012
comment
возможный дубликат функций сравнения с плавающей запятой для C#   -  person user7116    schedule 07.02.2012
comment
Итак, как бы вы сказали, что у меня есть два числа с плавающей запятой, оба из которых находятся в диапазоне от 1 до 2 миллионов; Я хочу, чтобы они были равны ближайшей тысяче?   -  person Eric Lippert    schedule 07.02.2012
comment
@EricLippert Обновил заголовок. В моем случае я на самом деле не забочусь о значащих цифрах, важны десятичные разряды. Но интересный вопрос!   -  person Kim    schedule 07.02.2012
comment
Что вы после этого? Сейчас непонятно. Вы хотите, чтобы 123.45 было равно 678.45?   -  person Mr Lister    schedule 07.02.2012
comment
@MrLister Моя первоначальная проблема заключалась в том, что я хотел, чтобы (0.1F + 0.2F) == 0.3F было истинным (что возвращает false в C#). Но реализацию можно использовать, чтобы сказать, что 123,123 почти равно 123,124 с двумя знаками после запятой. Меня не беспокоит, что 12 300 будут равны 12 400, если принять во внимание 2 значащие цифры. Вот что я имел в виду о десятичных разрядах.   -  person Kim    schedule 07.02.2012
comment
Хорошо... тогда вы имеете в виду, что 123.123 почти равно 123.120, но 123.124 не почти равно 123.126 (поскольку последнее округляется до 123.13)?   -  person Mr Lister    schedule 07.02.2012
comment
Невозможно, см. ответ ниже   -  person Thorsten S.    schedule 09.02.2012
comment
Какой результат вы хотите для AlmostEquals(0.149f, 0.151f, 1)?   -  person CodesInChaos    schedule 19.03.2012


Ответы (2)


Основываясь на ответе @infact и некоторых комментариях как к вопросу, так и к ответу, который я придумал

public static bool AlmostEquals(this float float1, float float2, int precision = 2) 
{ 
    float epsilon = Math.Pow(10.0, -precision) 
    return (Math.Abs(float1-float2) <= epsilon);   
} 

Преимущество этого заключается в том, что вы принимаете любое целое число, вы можете проверить точность> 1,0, используя точность = -x, где x - степень числа 10 для проверки.

Я бы также порекомендовал сделать точность по умолчанию = 3, что по умолчанию даст вам точность до одной десятой цента, если этот метод используется для финансовых расчетов.

person EtherDragon    schedule 07.02.2012
comment
Выбрал это как ответ, потому что он касается идеи о количестве знаков после запятой и расширяет метод, чтобы разрешить отрицательные значения для точности. - person Kim; 07.02.2012
comment
Поддерживает @infact для return (Math.Abs(float1-float2) <= precision); - person EtherDragon; 08.02.2012
comment
Он НЕ работает, в разделе C# даже приводится пример: msdn.microsoft.com/en-us/library/75ks3aby.aspx Из-за потери точности [...] метод Round(Double, Int32) может не округлять средние значения до ближайшего четное значение в десятичном разряде цифр. Это показано в следующем примере, где 2,135 округляется до 2,13 вместо 2,14. Это происходит из-за того, что внутри метод умножает значение на 10 цифр, и операция умножения в этом случае страдает от потери точности. - person Thorsten S.; 10.02.2012

Если пользователь хочет «сравнить два числа с плавающей запятой, используя заданное количество десятичных знаков (значащих цифр)», и это на самом деле означает, что у нас есть функция

Почти Равно(14.3XXXXXXXX, 14.3YYYYYYY, 1) == true для всех возможных XXX и YYY, а последний параметр — десятичный знак после запятой.

есть простой, но неудачный ответ:

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

Приведенные здесь решения уже нарушают почти равное (0.06f, 0.14f, 1) = true, но 0 != 1.

Почему ? Первая причина – крайняя чувствительность. Например: 0,0999999999.... и 0,100000...1 имеют во-первых разные цифры, но в разнице они почти неразличимы, они почти точно равны. Что бы ни делала мифическая функция, она не может допустить даже небольших различий в расчетах.

Вторая причина заключается в том, что мы хотим на самом деле вычислить с помощью чисел. Я использовал VC 2008 с C#, чтобы распечатать правильные значения функции Math.pow. Первый — это параметр точности, второй — шестнадцатеричное значение результирующего числа с плавающей запятой, а третий — точное десятичное значение.

1 3dcccccd 0.100000001490116119384765625

2 3c23d70a 0.00999999977648258209228515625

3 3a83126f 0.001000000047497451305389404296875

4 38d1b717 0.0000999999974737875163555145263671875

5 3727c5ac 0.00000999999974737875163555145263671875

6 358637bd 9.999999974752427078783512115478515625E-7

Как видите, последовательность 0,1, 0,01, 0,001 и т. д. дает числа, которые являются отличными приближениями, но либо слишком маленькими, либо слишком большими.

Что, если мы заставим данное место иметь правильную цифру? Давайте перечислим 16 двоичных значений для 4 бит.

0.0
0.0625
0.125
0.1875
0.25
0.3125
0.375
0.4375
0.5
0.5625
0.625
0.6875
0.75
0.8125
0.875
0.9375

16 различных двоичных чисел должно быть достаточно для 10 десятичных чисел, если мы хотим вычислять только с одним знаком после запятой. В то время как 0,5 точно равно, принудительное применение одной и той же десятичной цифры означает, что для 0,4 требуется 0,4375, а для 0,9 требуется 0,9375, что приводит к серьезным ошибкам.

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

В документации C# даже приводится пример: http://msdn.microsoft.com/en-us/library/75ks3aby.aspx

Примечания для вызывающих абонентов

Из-за потери точности, которая может возникнуть в результате представления десятичных значений в виде чисел с плавающей запятой или выполнения арифметических операций со значениями с плавающей запятой, в некоторых случаях метод Round(Double, Int32) может не округлять средние значения до ближайшего четного числа. значение в десятичном разряде цифр. Это показано в следующем примере, где 2,135 округляется до 2,13 вместо 2,14. Это происходит из-за того, что внутри метод умножает значение на 10 цифр, и операция умножения в этом случае страдает от потери точности.

person Thorsten S.    schedule 09.02.2012
comment
Ваша предпосылка неверна. Хотя числа с плавающей запятой являются приближенными, безусловно, можно предсказать степень ошибки в этом приближении, и, зная это, вы также знаете, действительно ли ваше сравнение. - person Robert Harvey; 19.03.2012
comment
Можно предсказать степень ошибки для десятично-двоичного преобразования и обратно, но из-за проблемы необходимое разрешение может быть столь же маленьким, как наименьшее число с плавающей запятой, нарушая код. ОП хочет, чтобы почти равно (10.0,10.0XXXXXX,1) == true, допуская числа 10.0000000..1 и 10.0999999999999.... в качестве допустимого ввода. Подпрограммы преобразования с плавающей запятой выбирают следующую с плавающей запятой, но это может быть 9,999999999 или 11,10000001. Теперь с особой осторожностью вы можете программировать конструкторы, которые дают наименьшее подходящее десятичное число, но вы не можете добавлять и т. д., потому что это ломается. - person Thorsten S.; 19.03.2012
comment
+1 @ТорстенС. за очень изысканный ответ и разработку по этому вопросу. - person XAMlMAX; 19.02.2014