Функции сравнения с плавающей запятой для C#


может ли кто-нибудь указать (или показать) некоторые хорошие общие функции сравнения с плавающей запятой в C# для сравнения значений с плавающей запятой? Я хочу реализовать функции IsEqual,IsGreater an IsLess. Я также действительно забочусь только о двойниках, а не о поплавках.

11 59

11 ответов:

написание полезной универсальной плавающей точки IsEqual очень, очень трудно, если не сказать невозможно. Ваш текущий код будет плохо работать для a==0. То, как метод должен вести себя в таких случаях, действительно является вопросом определения, и, возможно, код лучше всего будет адаптирован для конкретного случая использования домена.

для такого рода вещи, вы очень нужно хороший набор тестов. Вот как я это сделал для Руководство С Плавающей Запятой, это то, что я придумал в конце концов (Java-код, должно быть достаточно легко перевести):

public static boolean nearlyEqual(float a, float b, float epsilon) {
    final float absA = Math.abs(a);
    final float absB = Math.abs(b);
    final float diff = Math.abs(a - b);

    if (a == b) { // shortcut, handles infinities
        return true;
    } else if (a == 0 || b == 0 || diff < Float.MIN_NORMAL) {
        // a or b is zero or both are extremely close to it
        // relative error is less meaningful here
        return diff < (epsilon * Float.MIN_NORMAL);
    } else { // use relative error
        return diff / (absA + absB) < epsilon;
    }
}

вы можете найти набор тестов на сайте.

приложение: тот же код в c# для двойников (как задано в вопросах)

public static bool NearlyEqual(double a, double b, double epsilon)
{
    double absA = Math.Abs(a);
    double absB = Math.Abs(b);
    double diff = Math.Abs(a - b);

    if (a == b)
    { // shortcut, handles infinities
        return true;
    } 
    else if (a == 0 || b == 0 || diff < Double.Epsilon) 
    {
        // a or b is zero or both are extremely close to it
        // relative error is less meaningful here
        return diff < epsilon;
    }
    else
    { // use relative error
        return diff / (absA + absB) < epsilon;
    }
}

С статья Брюса Доусона о сравнении поплавков, вы также можете сравнить поплавки как целые числа. Близость определяется наименьшими значащими битами.

public static bool AlmostEqual2sComplement( float a, float b, int maxDeltaBits ) 
{
    int aInt = BitConverter.ToInt32( BitConverter.GetBytes( a ), 0 );
    if ( aInt <  0 )
        aInt = Int32.MinValue - aInt;  // Int32.MinValue = 0x80000000

    int bInt = BitConverter.ToInt32( BitConverter.GetBytes( b ), 0 );
    if ( bInt < 0 )
        bInt = Int32.MinValue - bInt;

    int intDiff = Math.Abs( aInt - bInt );
    return intDiff <= ( 1 << maxDeltaBits );
}

EDIT: BitConverter работает относительно медленно. Если вы готовы использовать небезопасный код, то вот очень быстрая версия:

    public static unsafe int FloatToInt32Bits( float f )
    {
        return *( (int*)&f );
    }

    public static bool AlmostEqual2sComplement( float a, float b, int maxDeltaBits )
    {
        int aInt = FloatToInt32Bits( a );
        if ( aInt < 0 )
            aInt = Int32.MinValue - aInt;

        int bInt = FloatToInt32Bits( b );
        if ( bInt < 0 )
            bInt = Int32.MinValue - bInt;

        int intDiff = Math.Abs( aInt - bInt );
        return intDiff <= ( 1 << maxDeltaBits );
    }

далее к ответу Эндрю Ванга: если метод BitConverter слишком медленный, но вы не можете использовать небезопасный код в своем проекте, эта структура ~6x быстрее, чем BitConverter:

[StructLayout(LayoutKind.Explicit)]
public struct FloatToIntSafeBitConverter
{
    public static int Convert(float value)
    {
        return new FloatToIntSafeBitConverter(value).IntValue;
    }

    public FloatToIntSafeBitConverter(float floatValue): this()
    {
        FloatValue = floatValue;
    }

    [FieldOffset(0)]
    public readonly int IntValue;

    [FieldOffset(0)]
    public readonly float FloatValue;
}

(кстати, я попытался использовать принятое решение, но оно (по крайней мере, мое преобразование) не удалось выполнить некоторые из модульных тестов, также упомянутых в ответе. например,assertTrue(nearlyEqual(Float.MIN_VALUE, -Float.MIN_VALUE));)

продолжая ответы Михаил и тестирование, важно иметь в виду при переводе исходного кода Java на C#, что Java и C# определяют свои константы по-разному. В C#, например, отсутствует постоянное содержание в Java, и определения параметра minvalue сильно отличаются.

Java определяет MIN_VALUE как наименьшее возможное положительное значение, в то время как C# определяет его как наименьшее возможное представимое значение в целом. Этот эквивалентное значение в C# - Эпсилон.

отсутствие MIN_NORMAL проблематично для прямого перевода исходного алгоритма - без него вещи начинают ломаться при малых значениях, близких к нулю. MIN_NORMAL Java следует спецификации IEEE наименьшего возможного числа, не имея ведущего бита significand как ноль, и с учетом этого мы можем определить наши собственные нормали как для одиночных, так и для двойных (которые dbc упоминал в комментариях к исходному ответу).

следующий код C# для одиночных проходит все тесты, приведенные в руководстве с плавающей запятой, а двойное издание проходит все тесты с незначительными изменениями в тестовых случаях для учета повышенной точности.

public static bool ApproximatelyEqualEpsilon(float a, float b, float epsilon)
{
    const float floatNormal = (1 << 23) * float.Epsilon;
    float absA = Math.Abs(a);
    float absB = Math.Abs(b);
    float diff = Math.Abs(a - b);

    if (a == b)
    {
        // Shortcut, handles infinities
        return true;
    }

    if (a == 0.0f || b == 0.0f || diff < floatNormal)
    {    
        // a or b is zero, or both are extremely close to it.
        // relative error is less meaningful here
        return diff < (epsilon * floatNormal);
    }

    // use relative error
    return diff / Math.Min((absA + absB), float.MaxValue) < epsilon;
}

версия для двойников идентична сохранить для изменения типа и что нормальный определяется как это вместо этого.

const double doubleNormal = (1L << 52) * double.Epsilon;

будьте осторожны с некоторыми ответами...

1 - Вы можете легко представить любое число с 15 значащими цифрами в памяти с двойным. Смотрите Википедия.

2-проблема исходит из расчета плавающих чисел, где вы можете потерять некоторую точность. Я имею в виду, что число вроде .Я мог бы стать чем-то вроде этого .1000000000000001 ==> после расчета. Когда вы делаете некоторые вычисления, результаты могут быть усечены, чтобы быть представлены в двойном формате. Это усечение приносит ошибку, которую вы могли бы получить.

3-чтобы предотвратить проблему при сравнении двойных значений, люди вводят погрешность, часто называемую Эпсилон. Если 2 плавающих числа имеют только контекстуальную разницу epsilon ha, то они считаются равными. Эпсилон никогда не бывает двойным.Ипсилон.

4 - Эпсилон никогда не двойные.Ипсилон. Он всегда больше, чем это. Многие народы думают, что она двойная.Но они действительно ошибаются. Чтобы иметь отличный ответ пожалуйста, смотрите: Hans Passant answer. Эпсилон основан на вашем контексте, где он зависит от наибольшего числа, которое вы достигаете во время расчета, и от количества выполняемых вычислений (накапливается ошибка усечения). Epsilon-это наименьшее число, которое вы можете представить в своем контексте с помощью 15 цифр.

5 - это код, который я использую. Будьте осторожны, что я использую свой Эпсилон только для нескольких расчетов. В противном случае я умножаю свой Эпсилон на 10 или 100.

6 - Как отметил Свенл, вполне возможно, что мой Эпсилон недостаточно велик. Предлагаю прочитать комментарий Свенла. Кроме того, возможно, "decimal" может выполнить эту работу для вашего случая?

public static class DoubleExtension
    {
        // ******************************************************************
        // Base on Hans Passant Answer on:
        // https://stackoverflow.com/questions/2411392/double-epsilon-for-equality-greater-than-less-than-less-than-or-equal-to-gre

        /// <summary>
        /// Compare two double taking in account the double precision potential error.
        /// Take care: truncation errors accumulate on calculation. More you do, more you should increase the epsilon.
        public static bool AboutEquals(this double value1, double value2)
        {
            double epsilon = Math.Max(Math.Abs(value1), Math.Abs(value2)) * 1E-15;
            return Math.Abs(value1 - value2) <= epsilon;
        }

        // ******************************************************************
        // Base on Hans Passant Answer on:
        // https://stackoverflow.com/questions/2411392/double-epsilon-for-equality-greater-than-less-than-less-than-or-equal-to-gre

        /// <summary>
        /// Compare two double taking in account the double precision potential error.
        /// Take care: truncation errors accumulate on calculation. More you do, more you should increase the epsilon.
        /// You get really better performance when you can determine the contextual epsilon first.
        /// </summary>
        /// <param name="value1"></param>
        /// <param name="value2"></param>
        /// <param name="precalculatedContextualEpsilon"></param>
        /// <returns></returns>
        public static bool AboutEquals(this double value1, double value2, double precalculatedContextualEpsilon)
        {
            return Math.Abs(value1 - value2) <= precalculatedContextualEpsilon;
        }

        // ******************************************************************
        public static double GetContextualEpsilon(this double biggestPossibleContextualValue)
        {
            return biggestPossibleContextualValue * 1E-15;
        }

        // ******************************************************************
        /// <summary>
        /// Mathlab equivalent
        /// </summary>
        /// <param name="dividend"></param>
        /// <param name="divisor"></param>
        /// <returns></returns>
        public static double Mod(this double dividend, double divisor)
        {
            return dividend - System.Math.Floor(dividend / divisor) * divisor;
        }

        // ******************************************************************
    }

вот сильно Расширенная версия класса Саймона Хьюитта:

/// <summary>
/// Safely converts a <see cref="float"/> to an <see cref="int"/> for floating-point comparisons.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct FloatToInt : IEquatable<FloatToInt>, IEquatable<float>, IEquatable<int>, IComparable<FloatToInt>, IComparable<float>, IComparable<int>
{
    /// <summary>
    /// Initializes a new instance of the <see cref="FloatToInt"/> class.
    /// </summary>
    /// <param name="floatValue">The <see cref="float"/> value to be converted to an <see cref="int"/>.</param>
    public FloatToInt(float floatValue)
        : this()
    {
        FloatValue = floatValue;
    }

    /// <summary>
    /// Gets the floating-point value as an integer.
    /// </summary>
    [FieldOffset(0)]
    public readonly int IntValue;

    /// <summary>
    /// Gets the floating-point value.
    /// </summary>
    [FieldOffset(0)]
    public readonly float FloatValue;

    /// <summary>
    /// Indicates whether the current object is equal to another object of the same type.
    /// </summary>
    /// <returns>
    /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
    /// </returns>
    /// <param name="other">An object to compare with this object.</param>
    public bool Equals(FloatToInt other)
    {
        return other.IntValue == IntValue;
    }

    /// <summary>
    /// Indicates whether the current object is equal to another object of the same type.
    /// </summary>
    /// <returns>
    /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
    /// </returns>
    /// <param name="other">An object to compare with this object.</param>
    public bool Equals(float other)
    {
        return IntValue == new FloatToInt(other).IntValue;
    }

    /// <summary>
    /// Indicates whether the current object is equal to another object of the same type.
    /// </summary>
    /// <returns>
    /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
    /// </returns>
    /// <param name="other">An object to compare with this object.</param>
    public bool Equals(int other)
    {
        return IntValue == other;
    }

    /// <summary>
    /// Compares the current object with another object of the same type.
    /// </summary>
    /// <returns>
    /// A value that indicates the relative order of the objects being compared. The return value has the following meanings: Value Meaning Less than zero This object is less than the <paramref name="other"/> parameter.Zero This object is equal to <paramref name="other"/>. Greater than zero This object is greater than <paramref name="other"/>. 
    /// </returns>
    /// <param name="other">An object to compare with this object.</param>
    public int CompareTo(FloatToInt other)
    {
        return IntValue.CompareTo(other.IntValue);
    }

    /// <summary>
    /// Compares the current object with another object of the same type.
    /// </summary>
    /// <returns>
    /// A value that indicates the relative order of the objects being compared. The return value has the following meanings: Value Meaning Less than zero This object is less than the <paramref name="other"/> parameter.Zero This object is equal to <paramref name="other"/>. Greater than zero This object is greater than <paramref name="other"/>. 
    /// </returns>
    /// <param name="other">An object to compare with this object.</param>
    public int CompareTo(float other)
    {
        return IntValue.CompareTo(new FloatToInt(other).IntValue);
    }

    /// <summary>
    /// Compares the current object with another object of the same type.
    /// </summary>
    /// <returns>
    /// A value that indicates the relative order of the objects being compared. The return value has the following meanings: Value Meaning Less than zero This object is less than the <paramref name="other"/> parameter.Zero This object is equal to <paramref name="other"/>. Greater than zero This object is greater than <paramref name="other"/>. 
    /// </returns>
    /// <param name="other">An object to compare with this object.</param>
    public int CompareTo(int other)
    {
        return IntValue.CompareTo(other);
    }

    /// <summary>
    /// Indicates whether this instance and a specified object are equal.
    /// </summary>
    /// <returns>
    /// true if <paramref name="obj"/> and this instance are the same type and represent the same value; otherwise, false.
    /// </returns>
    /// <param name="obj">Another object to compare to. </param><filterpriority>2</filterpriority>
    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj))
        {
            return false;
        }
        if (obj.GetType() != typeof(FloatToInt))
        {
            return false;
        }
        return Equals((FloatToInt)obj);
    }

    /// <summary>
    /// Returns the hash code for this instance.
    /// </summary>
    /// <returns>
    /// A 32-bit signed integer that is the hash code for this instance.
    /// </returns>
    /// <filterpriority>2</filterpriority>
    public override int GetHashCode()
    {
        return IntValue;
    }

    /// <summary>
    /// Implicitly converts from a <see cref="FloatToInt"/> to an <see cref="int"/>.
    /// </summary>
    /// <param name="value">A <see cref="FloatToInt"/>.</param>
    /// <returns>An integer representation of the floating-point value.</returns>
    public static implicit operator int(FloatToInt value)
    {
        return value.IntValue;
    }

    /// <summary>
    /// Implicitly converts from a <see cref="FloatToInt"/> to a <see cref="float"/>.
    /// </summary>
    /// <param name="value">A <see cref="FloatToInt"/>.</param>
    /// <returns>The floating-point value.</returns>
    public static implicit operator float(FloatToInt value)
    {
        return value.FloatValue;
    }

    /// <summary>
    /// Determines if two <see cref="FloatToInt"/> instances have the same integer representation.
    /// </summary>
    /// <param name="left">A <see cref="FloatToInt"/>.</param>
    /// <param name="right">A <see cref="FloatToInt"/>.</param>
    /// <returns>true if the two <see cref="FloatToInt"/> have the same integer representation; otherwise, false.</returns>
    public static bool operator ==(FloatToInt left, FloatToInt right)
    {
        return left.IntValue == right.IntValue;
    }

    /// <summary>
    /// Determines if two <see cref="FloatToInt"/> instances have different integer representations.
    /// </summary>
    /// <param name="left">A <see cref="FloatToInt"/>.</param>
    /// <param name="right">A <see cref="FloatToInt"/>.</param>
    /// <returns>true if the two <see cref="FloatToInt"/> have different integer representations; otherwise, false.</returns>
    public static bool operator !=(FloatToInt left, FloatToInt right)
    {
        return !(left == right);
    }
}

вот как я решил это, с нулевым методом двойного расширения.

    public static bool NearlyEquals(this double? value1, double? value2, double unimportantDifference = 0.0001)
    {
        if (value1 != value2)
        {
            if(value1 == null || value2 == null)
                return false;

            return Math.Abs(value1.Value - value2.Value) < unimportantDifference;
        }

        return true;
    }

...

        double? value1 = 100;
        value1.NearlyEquals(100.01); // will return false
        value1.NearlyEquals(100.000001); // will return true
        value1.NearlyEquals(100.01, 0.1); // will return true

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

Я перевел образец от Майкл Borgwardt. Вот результат:

public static bool NearlyEqual(float a, float b, float epsilon){
    float absA = Math.Abs (a);
    float absB = Math.Abs (b);
    float diff = Math.Abs (a - b);

    if (a == b) {
        return true;
    } else if (a == 0 || b == 0 || diff < float.Epsilon) {
        // a or b is zero or both are extremely close to it
        // relative error is less meaningful here
        return diff < epsilon;
    } else { // use relative error
        return diff / (absA + absB) < epsilon;
    }
}

Не стесняйтесь улучшить этот ответ.

Я думаю, что второй вариант является лучшим выбором. Обычно при сравнении с плавающей запятой вы часто заботитесь только о том, что одно значение находится в пределах определенного допуска другого значения, контролируемого выбором Эпсилона.

о: b - delta < a && a < b + delta