22

I have a unit test, testing boundaries:

[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void CreateExtent_InvalidTop_ShouldThrowArgumentOutOfRangeException()
{
    var invalidTop = 90.0 + Double.Epsilon;
    new Extent(invalidTop, 0.0, 0.0, 0.0);
}

public static readonly double MAX_LAT = 90.0;

public Extent(double top, double right, double bottom, double left)
{
    if (top > GeoConstants.MAX_LAT)
        throw new ArgumentOutOfRangeException("top"); // not hit
}

I thought I'd just tip the 90.0 over the edge by adding the minimum possible positive double to it, but now the exception is not thrown, any idea why?

When debugging, I see top as coming in as 90, when it should be 90.00000000.... something.

EDIT: I should have thought a bit harder, 90+Double.Epsilon will lose its resolution. Seems the best way to go is do some bit shifting.

SOLUTION:

[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void CreateExtent_InvalidTop_ShouldThrowArgumentOutOfRangeException()
{
    var invalidTop = Utility.IncrementTiny(90); // 90.000000000000014
    // var sameAsEpsilon = Utility.IncrementTiny(0);
    new Extent(invalidTop, 0, 0, 0);
}

/// <summary>
/// Increment a double-precision number by the smallest amount possible
/// </summary>
/// <param name="number">double-precision number</param>
/// <returns>incremented number</returns>
public static double IncrementTiny(double number)
{
    #region SANITY CHECKS
    if (Double.IsNaN(number) || Double.IsInfinity(number))
        throw new ArgumentOutOfRangeException("number");
    #endregion

    var bits = BitConverter.DoubleToInt64Bits(number);

    // if negative then go opposite way
    if (number > 0)
        return BitConverter.Int64BitsToDouble(bits + 1);
    else if (number < 0)
        return BitConverter.Int64BitsToDouble(bits - 1);
    else
        return Double.Epsilon;
}

/// <summary>
/// Decrement a double-precision number by the smallest amount possible
/// </summary>
/// <param name="number">double-precision number</param>
/// <returns>decremented number</returns>
public static double DecrementTiny(double number)
{
    #region SANITY CHECKS
    if (Double.IsNaN(number) || Double.IsInfinity(number))
        throw new ArgumentOutOfRangeException("number");
    #endregion

    var bits = BitConverter.DoubleToInt64Bits(number);

    // if negative then go opposite way
    if (number > 0)
        return BitConverter.Int64BitsToDouble(bits - 1);
    else if (number < 0)
        return BitConverter.Int64BitsToDouble(bits + 1);
    else
        return 0 - Double.Epsilon;
}

This does the job.

sprocket12
  • 5,368
  • 18
  • 64
  • 133
  • 1
    Double precision is a nasty business, but when comparing the maximum deviation between A and B is `Double.Epsilon`, so you've probably not tipped it enough by a very, very small margin. – jessehouwing Dec 16 '14 at 14:09
  • See: http://stackoverflow.com/questions/2411392/double-epsilon-for-equality-greater-than-less-than-less-than-or-equal-to-gre – Habib Dec 16 '14 at 14:12
  • Interesting article here: http://www.johndcook.com/blog/2012/01/05/double-epsilon-dbl_epsilon/ The TL;DR is "`Double.Epsilon` isn't as useful as you might think!" – AAT Dec 16 '14 at 14:17
  • Bruce Dawson has [a great series of articles on ULPs and comparing floats and doubles](https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/). His example code tends to be in C++ but the articles are mostly explanation. – cartographer Dec 16 '14 at 18:12
  • This is probably fine for your purposes (though I'm not sure it works correctly if you want to do the equivalent test for a negative value), but people adapting it for other purposes should consider: Does this function behave reasonably when presented with infinity, nan, maxvalue, zero, denormalized, or negative numbers, and if not, do you care? – Random832 Dec 16 '14 at 19:56
  • Read "_What Every Computer Scientist Should Know About Floating-Point Arithmetic_", or any of the various related discussions that appear when you Google that. – geometrian Dec 17 '14 at 03:01
  • It doesn't -- the equality check just returns true for incredibly small deviations (even deviations much bigger than `double.Epsilon`). This is part of IEE754. (It's also why "-0" and "+0" compare as equal even though they're defined is being different in binary, and treated differently in division.) – BrainSlugs83 Jul 05 '16 at 19:30
  • (^ It's also the reason why `double.Epsilon == 0d` returns true.) – BrainSlugs83 Jul 05 '16 at 19:42

4 Answers4

26

Per the documentation of Double.Epsilon:

The value of the Epsilon property reflects the smallest positive Double value that is significant in numeric operations or comparisons when the value of the Double instance is zero.

(Emphasis mine.)

Adding it to 90.0 does not produce "the next smallest value after 90.0", this just yields 90.0 again.

Jeroen Mostert
  • 27,176
  • 2
  • 52
  • 85
  • 1
    There's not one number that works in any situation. `double` has a maximum of 15 digits of precision, so a tiny number that can be added to 1 and result a "different" `double` can't be added to 1,000,000 and still get a "different" `double`. – D Stanley Dec 16 '14 at 14:14
  • @Muhammad: That's a question all on its own. See http://stackoverflow.com/questions/155378/how-to-alter-a-float-by-its-smallest-increment-or-close-to-it and http://stackoverflow.com/questions/14278248/find-the-float-just-below-a-value . – Jeroen Mostert Dec 16 '14 at 14:14
21

Double.Epsilon is the smallest positive representable value. Just because it's representable on its own does not mean it's the smallest value between any other representable value and the next highest one.

Imagine you had a system to represent just integers. You can represent any integer to 5 significant figures, along with a scale (e.g. in the range 1-100).

So these values are exactly representable, for example

  • 12345 (digits=12345, scale = 0)
  • 12345000 (digits=12345, scale = 3)

In that system, the "epsilon" value would be 1... but if you add 1 to 12345000 you'd still end up with 12345000 because the system couldn't represent the exact result of 12345001.

Now apply the same logic to double, with all its intricacies, and you get a much smaller epsilon, but the same general principle: a value which is distinct from zero, but still can end up not making any difference when added to larger numbers.

Note that much larger values have the same property too - for example, if x is a very large double, then x + 1 may well be equal to x because the gap between two "adjacent" doubles becomes more than 2 as the values get big.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    @BrainSlugs83: Where are you seeing `double.Epsilon == 0d` returning true? It doesn't for me. – Jon Skeet Jul 05 '16 at 19:58
  • you're right! -- I swear it did a minute ago in a Console.WriteLine test, but it's definitely not right now -- too late to edit that comment, so I deleted it -- don't want to confuse anyone. Double checking [IEE754](https://en.wikipedia.org/wiki/IEEE_754-1985#Comparing_floating-point_numbers) right now to get my head on straight. – BrainSlugs83 Jul 05 '16 at 20:12
2

In C99 and C++, the function that does what you were trying to do is called nextafter and is in math.h. I do not know if C# has any equivalent, but if it does, I would expect it to have a similar name.

zwol
  • 135,547
  • 38
  • 252
  • 361
1

Because Double.Epsilon is the "smallest noticeable change" (loosely speaking) in a double number.

.. but this does not mean that it will have any effect when you use it.

As you know, floats/doubles vary in their resolution depending on the magnitude of the vlue they contain. For example, artificial:

  • ...
  • -100 -> +-0.1
  • -10 -> +-0.01
  • 0 -> +-0.001
  • 10 -> +-0.01
  • 100 -> +-0.1
  • ...

If the resolutions were like this, the Epsilon would be 0.001, as it's the smallest possible change. But what would be the expected result of 1000000 + 0.001 in such system?

quetzalcoatl
  • 32,194
  • 8
  • 68
  • 107
  • 1
    "Double.Epsilon is the smallest noticeable change in a double number" Not exactly true - it's the smallest double _greater than 0_ that can be represented as a `double`. There is no `double` value that that can be subtracted from 1.0 that will result in an answer equal to `double.Epsilon` – D Stanley Dec 16 '14 at 14:16
  • 1
    @DStanley: Thanks, added a clarification on that. I never meant to explain this part, just wanted to set readers' mind on the topic. – quetzalcoatl Dec 16 '14 at 14:20