From 5fb5b1f568bf87c24b4c15d4f88433c3a3c4e03e Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Mon, 24 Feb 2014 13:31:35 +0000 Subject: [PATCH] Fixed the problems the AreAlmostEqual() methods had with floating point zeros git-svn-id: file:///srv/devel/repo-conversion/nusu@294 d2e56fa2-650e-0410-a79f-9358c0239efd --- Source/FloatHelper.Test.cs | 107 ++++++++++++++++++++++++++++++------- Source/FloatHelper.cs | 68 ++++++++++++++++++----- 2 files changed, 143 insertions(+), 32 deletions(-) diff --git a/Source/FloatHelper.Test.cs b/Source/FloatHelper.Test.cs index 15a27c5..5e1bc5b 100644 --- a/Source/FloatHelper.Test.cs +++ b/Source/FloatHelper.Test.cs @@ -143,25 +143,80 @@ namespace Nuclex.Support { ); } - // http://www.altdevblogaday.com/2012/02/22/comparing-floating-point-numbers-2012-edition/ - // Make both positive - // If both are negative -> fine - // If both are positive -> fine - // If different -> Measure both distances to zero in ulps and sum them /// - /// Verifies that the negative floating point zero is within one ulp of the positive - /// floating point zero and vice versa + /// Verifies that two denormalized floats can be compared in ulps /// [Test] - public void NegativeZeroFloatEqualsPositiveZero() { + public void DenormalizedFloatsCanBeCompared() { float zero = 0.0f; float zeroPlusOneUlp = FloatHelper.ReinterpretAsFloat( FloatHelper.ReinterpretAsInt(zero) + 1 ); float zeroMinusOneUlp = -zeroPlusOneUlp; - bool test = FloatHelper.AreAlmostEqual(zeroMinusOneUlp, zeroPlusOneUlp, 1); + // Across zero + Assert.IsFalse(FloatHelper.AreAlmostEqual(zeroMinusOneUlp, zeroPlusOneUlp, 1)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(zeroPlusOneUlp, zeroMinusOneUlp, 2)); + // Against zero + Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 0)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 1)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 0)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 1)); + } + + /// + /// Verifies that the negative floating point zero is within one ulp of the positive + /// floating point zero and vice versa + /// + [Test] + public void NegativeZeroFloatEqualsPositiveZero() { + Assert.IsTrue( + FloatHelper.AreAlmostEqual( + FloatHelper.NegativeZeroFloat, FloatHelper.PositiveZeroFloat, 0 + ) + ); + Assert.IsTrue( + FloatHelper.AreAlmostEqual( + FloatHelper.PositiveZeroFloat, FloatHelper.NegativeZeroFloat, 0 + ) + ); + } + + /// Verifies that floats can be compared across the zero boundary + [Test] + public void FloatsCanBeComparedAcrossZeroInUlps() { + float tenUlps = float.Epsilon * 10.0f; + + Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 20)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, -tenUlps, 20)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 19)); + + Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, 0, 10)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(0, -tenUlps, 10)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, 0, 9)); + + Assert.IsTrue(FloatHelper.AreAlmostEqual(0, tenUlps, 10)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, 0, 10)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(0, tenUlps, 9)); + } + + /// + /// Verifies that two denormalized doubles can be compared in ulps + /// + [Test] + public void DenormalizedDoublesCanBeCompared() { + double zero = 0.0; + double zeroPlusOneUlp = FloatHelper.ReinterpretAsDouble( + FloatHelper.ReinterpretAsLong(zero) + 1 + ); + double zeroMinusOneUlp = -zeroPlusOneUlp; + + // Across zero + Assert.IsFalse(FloatHelper.AreAlmostEqual(zeroMinusOneUlp, zeroPlusOneUlp, 1)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(zeroPlusOneUlp, zeroMinusOneUlp, 2)); + + // Against zero Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 0)); Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 1)); Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 0)); @@ -174,18 +229,34 @@ namespace Nuclex.Support { /// [Test] public void NegativeZeroDoubleEqualsPositiveZero() { - double zero = 0.0; - double zeroPlusOneUlp = FloatHelper.ReinterpretAsDouble( - FloatHelper.ReinterpretAsLong(zero) + 1 + Assert.IsTrue( + FloatHelper.AreAlmostEqual( + FloatHelper.NegativeZeroDouble, FloatHelper.NegativeZeroDouble, 0 + ) ); - double zeroMinusOneUlp = -zeroPlusOneUlp; + Assert.IsTrue( + FloatHelper.AreAlmostEqual( + FloatHelper.NegativeZeroDouble, FloatHelper.NegativeZeroDouble, 0 + ) + ); + } - bool test = FloatHelper.AreAlmostEqual(zeroMinusOneUlp, zeroPlusOneUlp, 1); + /// Verifies that doubles can be compared across the zero boundary + [Test] + public void DoublesCanBeComparedAcrossZeroInUlps() { + double tenUlps = double.Epsilon * 10.0; - Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 0)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 1)); - Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 0)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 1)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 20)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, -tenUlps, 20)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 19)); + + Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, 0, 10)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(0, -tenUlps, 10)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, 0, 9)); + + Assert.IsTrue(FloatHelper.AreAlmostEqual(0, tenUlps, 10)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, 0, 10)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(0, tenUlps, 9)); } } diff --git a/Source/FloatHelper.cs b/Source/FloatHelper.cs index 0833753..1a09609 100644 --- a/Source/FloatHelper.cs +++ b/Source/FloatHelper.cs @@ -44,11 +44,11 @@ namespace Nuclex.Support { /// public static class FloatHelper { - #region struct FloatIntUnion + #region struct FloatInt32Union /// Union of a floating point variable and an integer [StructLayout(LayoutKind.Explicit)] - private struct FloatIntUnion { + private struct FloatInt32Union { /// The union's value as a floating point variable [FieldOffset(0)] @@ -64,13 +64,13 @@ namespace Nuclex.Support { } - #endregion // struct FloatIntUnion + #endregion // struct FloatInt32Union - #region struct DoubleLongUnion + #region struct DoubleInt64Union /// Union of a double precision floating point variable and a long [StructLayout(LayoutKind.Explicit)] - private struct DoubleLongUnion { + private struct DoubleInt64Union { /// The union's value as a double precision floating point variable [FieldOffset(0)] @@ -86,7 +86,7 @@ namespace Nuclex.Support { } - #endregion // struct DoubleLongUnion + #endregion // struct DoubleInt64Union /// A floating point value that holds a positive zero public const float PositiveZeroFloat = +0.0f; @@ -145,11 +145,30 @@ namespace Nuclex.Support { /// /// Implementation partially follows the code outlined here (link now defunct): /// http://www.anttirt.net/2007/08/19/proper-floating-point-comparisons/ + /// And here: + /// http://www.altdevblogaday.com/2012/02/22/comparing-floating-point-numbers-2012-edition/ /// /// public static bool AreAlmostEqual(float left, float right, int maxUlps) { - FloatIntUnion leftUnion = new FloatIntUnion(); - FloatIntUnion rightUnion = new FloatIntUnion(); + var leftUnion = new FloatInt32Union(); + var rightUnion = new FloatInt32Union(); + + leftUnion.Float = left; + rightUnion.Float = right; + + if(leftUnion.Int < 0) { + leftUnion.Int = unchecked((int)0x80000000 - leftUnion.Int); + } + if(rightUnion.Int < 0) { + rightUnion.Int = unchecked((int)0x80000000 - rightUnion.Int); + } + + return Math.Abs(rightUnion.Int - leftUnion.Int) <= maxUlps; + } +#if false + public static bool OldAreAlmostEqual(float left, float right, int maxUlps) { + FloatInt32Union leftUnion = new FloatInt32Union(); + FloatInt32Union rightUnion = new FloatInt32Union(); leftUnion.Float = left; rightUnion.Float = right; @@ -165,6 +184,7 @@ namespace Nuclex.Support { return (Math.Abs(leftUnion.Int - rightUnion.Int) <= maxUlps); } +#endif /// Compares two double precision floating point values for equality /// First double precision floating point value to be compared @@ -189,11 +209,30 @@ namespace Nuclex.Support { /// /// Implementation partially follows the code outlined here: /// http://www.anttirt.net/2007/08/19/proper-floating-point-comparisons/ + /// And here: + /// http://www.altdevblogaday.com/2012/02/22/comparing-floating-point-numbers-2012-edition/ /// /// public static bool AreAlmostEqual(double left, double right, long maxUlps) { - DoubleLongUnion leftUnion = new DoubleLongUnion(); - DoubleLongUnion rightUnion = new DoubleLongUnion(); + var leftUnion = new DoubleInt64Union(); + var rightUnion = new DoubleInt64Union(); + + leftUnion.Double = left; + rightUnion.Double = right; + + if(leftUnion.Long < 0) { + leftUnion.Long = unchecked((long)0x8000000000000000 - leftUnion.Long); + } + if(rightUnion.Long < 0) { + rightUnion.Long = unchecked((long)0x8000000000000000 - rightUnion.Long); + } + + return Math.Abs(rightUnion.Long - leftUnion.Long) <= maxUlps; + } +#if false + public static bool OldAreAlmostEqual(double left, double right, long maxUlps) { + DoubleInt64Union leftUnion = new DoubleInt64Union(); + DoubleInt64Union rightUnion = new DoubleInt64Union(); leftUnion.Double = left; rightUnion.Double = right; @@ -209,6 +248,7 @@ namespace Nuclex.Support { return (Math.Abs(leftUnion.Long - rightUnion.Long) <= maxUlps); } +#endif /// /// Reinterprets the memory contents of a floating point value as an integer value @@ -220,7 +260,7 @@ namespace Nuclex.Support { /// The memory contents of the floating point value interpreted as an integer /// public static int ReinterpretAsInt(this float value) { - FloatIntUnion union = new FloatIntUnion(); + FloatInt32Union union = new FloatInt32Union(); union.Float = value; return union.Int; } @@ -237,7 +277,7 @@ namespace Nuclex.Support { /// interpreted as an integer /// public static long ReinterpretAsLong(this double value) { - DoubleLongUnion union = new DoubleLongUnion(); + DoubleInt64Union union = new DoubleInt64Union(); union.Double = value; return union.Long; } @@ -250,7 +290,7 @@ namespace Nuclex.Support { /// The memory contents of the integer value interpreted as a floating point value /// public static float ReinterpretAsFloat(this int value) { - FloatIntUnion union = new FloatIntUnion(); + FloatInt32Union union = new FloatInt32Union(); union.Int = value; return union.Float; } @@ -265,7 +305,7 @@ namespace Nuclex.Support { /// floating point value /// public static double ReinterpretAsDouble(this long value) { - DoubleLongUnion union = new DoubleLongUnion(); + DoubleInt64Union union = new DoubleInt64Union(); union.Long = value; return union.Double; }