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
This commit is contained in:
Markus Ewald 2014-02-24 13:31:35 +00:00
parent 0a7306bb58
commit 5fb5b1f568
2 changed files with 143 additions and 32 deletions

View File

@ -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
/// <summary> /// <summary>
/// Verifies that the negative floating point zero is within one ulp of the positive /// Verifies that two denormalized floats can be compared in ulps
/// floating point zero and vice versa
/// </summary> /// </summary>
[Test] [Test]
public void NegativeZeroFloatEqualsPositiveZero() { public void DenormalizedFloatsCanBeCompared() {
float zero = 0.0f; float zero = 0.0f;
float zeroPlusOneUlp = FloatHelper.ReinterpretAsFloat( float zeroPlusOneUlp = FloatHelper.ReinterpretAsFloat(
FloatHelper.ReinterpretAsInt(zero) + 1 FloatHelper.ReinterpretAsInt(zero) + 1
); );
float zeroMinusOneUlp = -zeroPlusOneUlp; 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));
}
/// <summary>
/// Verifies that the negative floating point zero is within one ulp of the positive
/// floating point zero and vice versa
/// </summary>
[Test]
public void NegativeZeroFloatEqualsPositiveZero() {
Assert.IsTrue(
FloatHelper.AreAlmostEqual(
FloatHelper.NegativeZeroFloat, FloatHelper.PositiveZeroFloat, 0
)
);
Assert.IsTrue(
FloatHelper.AreAlmostEqual(
FloatHelper.PositiveZeroFloat, FloatHelper.NegativeZeroFloat, 0
)
);
}
/// <summary>Verifies that floats can be compared across the zero boundary</summary>
[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));
}
/// <summary>
/// Verifies that two denormalized doubles can be compared in ulps
/// </summary>
[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.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 0));
Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 1)); Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 1));
Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 0)); Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 0));
@ -174,18 +229,34 @@ namespace Nuclex.Support {
/// </summary> /// </summary>
[Test] [Test]
public void NegativeZeroDoubleEqualsPositiveZero() { public void NegativeZeroDoubleEqualsPositiveZero() {
double zero = 0.0; Assert.IsTrue(
double zeroPlusOneUlp = FloatHelper.ReinterpretAsDouble( FloatHelper.AreAlmostEqual(
FloatHelper.ReinterpretAsLong(zero) + 1 FloatHelper.NegativeZeroDouble, FloatHelper.NegativeZeroDouble, 0
)
); );
double zeroMinusOneUlp = -zeroPlusOneUlp; Assert.IsTrue(
FloatHelper.AreAlmostEqual(
FloatHelper.NegativeZeroDouble, FloatHelper.NegativeZeroDouble, 0
)
);
}
bool test = FloatHelper.AreAlmostEqual(zeroMinusOneUlp, zeroPlusOneUlp, 1); /// <summary>Verifies that doubles can be compared across the zero boundary</summary>
[Test]
public void DoublesCanBeComparedAcrossZeroInUlps() {
double tenUlps = double.Epsilon * 10.0;
Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 0)); Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 20));
Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 1)); Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, -tenUlps, 20));
Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 0)); Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 19));
Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 1));
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));
} }
} }

View File

@ -44,11 +44,11 @@ namespace Nuclex.Support {
/// </remarks> /// </remarks>
public static class FloatHelper { public static class FloatHelper {
#region struct FloatIntUnion #region struct FloatInt32Union
/// <summary>Union of a floating point variable and an integer</summary> /// <summary>Union of a floating point variable and an integer</summary>
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]
private struct FloatIntUnion { private struct FloatInt32Union {
/// <summary>The union's value as a floating point variable</summary> /// <summary>The union's value as a floating point variable</summary>
[FieldOffset(0)] [FieldOffset(0)]
@ -64,13 +64,13 @@ namespace Nuclex.Support {
} }
#endregion // struct FloatIntUnion #endregion // struct FloatInt32Union
#region struct DoubleLongUnion #region struct DoubleInt64Union
/// <summary>Union of a double precision floating point variable and a long</summary> /// <summary>Union of a double precision floating point variable and a long</summary>
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]
private struct DoubleLongUnion { private struct DoubleInt64Union {
/// <summary>The union's value as a double precision floating point variable</summary> /// <summary>The union's value as a double precision floating point variable</summary>
[FieldOffset(0)] [FieldOffset(0)]
@ -86,7 +86,7 @@ namespace Nuclex.Support {
} }
#endregion // struct DoubleLongUnion #endregion // struct DoubleInt64Union
/// <summary>A floating point value that holds a positive zero</summary> /// <summary>A floating point value that holds a positive zero</summary>
public const float PositiveZeroFloat = +0.0f; public const float PositiveZeroFloat = +0.0f;
@ -145,11 +145,30 @@ namespace Nuclex.Support {
/// <para> /// <para>
/// Implementation partially follows the code outlined here (link now defunct): /// Implementation partially follows the code outlined here (link now defunct):
/// http://www.anttirt.net/2007/08/19/proper-floating-point-comparisons/ /// 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/
/// </para> /// </para>
/// </remarks> /// </remarks>
public static bool AreAlmostEqual(float left, float right, int maxUlps) { public static bool AreAlmostEqual(float left, float right, int maxUlps) {
FloatIntUnion leftUnion = new FloatIntUnion(); var leftUnion = new FloatInt32Union();
FloatIntUnion rightUnion = new FloatIntUnion(); 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; leftUnion.Float = left;
rightUnion.Float = right; rightUnion.Float = right;
@ -165,6 +184,7 @@ namespace Nuclex.Support {
return (Math.Abs(leftUnion.Int - rightUnion.Int) <= maxUlps); return (Math.Abs(leftUnion.Int - rightUnion.Int) <= maxUlps);
} }
#endif
/// <summary>Compares two double precision floating point values for equality</summary> /// <summary>Compares two double precision floating point values for equality</summary>
/// <param name="left">First double precision floating point value to be compared</param> /// <param name="left">First double precision floating point value to be compared</param>
@ -189,11 +209,30 @@ namespace Nuclex.Support {
/// <para> /// <para>
/// Implementation partially follows the code outlined here: /// Implementation partially follows the code outlined here:
/// http://www.anttirt.net/2007/08/19/proper-floating-point-comparisons/ /// 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/
/// </para> /// </para>
/// </remarks> /// </remarks>
public static bool AreAlmostEqual(double left, double right, long maxUlps) { public static bool AreAlmostEqual(double left, double right, long maxUlps) {
DoubleLongUnion leftUnion = new DoubleLongUnion(); var leftUnion = new DoubleInt64Union();
DoubleLongUnion rightUnion = new DoubleLongUnion(); 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; leftUnion.Double = left;
rightUnion.Double = right; rightUnion.Double = right;
@ -209,6 +248,7 @@ namespace Nuclex.Support {
return (Math.Abs(leftUnion.Long - rightUnion.Long) <= maxUlps); return (Math.Abs(leftUnion.Long - rightUnion.Long) <= maxUlps);
} }
#endif
/// <summary> /// <summary>
/// Reinterprets the memory contents of a floating point value as an integer value /// 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 /// The memory contents of the floating point value interpreted as an integer
/// </returns> /// </returns>
public static int ReinterpretAsInt(this float value) { public static int ReinterpretAsInt(this float value) {
FloatIntUnion union = new FloatIntUnion(); FloatInt32Union union = new FloatInt32Union();
union.Float = value; union.Float = value;
return union.Int; return union.Int;
} }
@ -237,7 +277,7 @@ namespace Nuclex.Support {
/// interpreted as an integer /// interpreted as an integer
/// </returns> /// </returns>
public static long ReinterpretAsLong(this double value) { public static long ReinterpretAsLong(this double value) {
DoubleLongUnion union = new DoubleLongUnion(); DoubleInt64Union union = new DoubleInt64Union();
union.Double = value; union.Double = value;
return union.Long; return union.Long;
} }
@ -250,7 +290,7 @@ namespace Nuclex.Support {
/// The memory contents of the integer value interpreted as a floating point value /// The memory contents of the integer value interpreted as a floating point value
/// </returns> /// </returns>
public static float ReinterpretAsFloat(this int value) { public static float ReinterpretAsFloat(this int value) {
FloatIntUnion union = new FloatIntUnion(); FloatInt32Union union = new FloatInt32Union();
union.Int = value; union.Int = value;
return union.Float; return union.Float;
} }
@ -265,7 +305,7 @@ namespace Nuclex.Support {
/// floating point value /// floating point value
/// </returns> /// </returns>
public static double ReinterpretAsDouble(this long value) { public static double ReinterpretAsDouble(this long value) {
DoubleLongUnion union = new DoubleLongUnion(); DoubleInt64Union union = new DoubleInt64Union();
union.Long = value; union.Long = value;
return union.Double; return union.Double;
} }