Функции сравнения с плавающей запятой для C#
может ли кто-нибудь указать (или показать) некоторые хорошие общие функции сравнения с плавающей запятой в C# для сравнения значений с плавающей запятой? Я хочу реализовать функции IsEqual
,IsGreater
an IsLess
. Я также действительно забочусь только о двойниках, а не о поплавках.
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; } }
Не стесняйтесь улучшить этот ответ.