1

I am doing a simple comparison between a string value and floats minValue and maxValue to check if value is outside of a certain range

if float(value) < minValue or float(value) > maxValue:
    # show error and exit

It works fine unless value and minValue are the same. So when value = -1.234 and minValue = -1.234, it goes inside the if statement because for some reason -1.234 < -1.234 evaluates to True.

My workaround is to use Decimal

if Decimal(value) < Decimal(str(minValue)) or Decimal(str(value) > Decimal(maxValue):
    # show error and exit

But Decimal(str(minValue)) looks a little messy so I'm wondering if there is a better way of comparing two floats that won't fail when they are the same value without having to do float to string to decimal conversion first.

EDIT

value comes from a csv file, so there is no truncation there. What you see is what you get. minValue is, however, a result of a polynomial function, but I don't believe there is any truncation or rounding there.

Here's the function to calculate minValue

f = numpy.poly1d(coefficients)
x = range(int(low), int(high), 1)
y = f(x)
minValue = min(y)
maxValue = max(y)
Community
  • 1
  • 1
Max
  • 41
  • 5
  • Without seeing how you get the values, it's hard to answer this question. See https://stackoverflow.com/q/588004/2988730 – Mad Physicist Jan 25 '19 at 17:57
  • 3
    Are you sure that `value` and `minValue` are both _exactly_ `-1.234`? If you're thinking "yes, I know because I printed both of them", keep in mind that there are many float values which are only slightly larger or smaller than -1.234, and thus get printed as "-1.234" when some of their digits are truncated. – Kevin Jan 25 '19 at 17:57
  • Related: https://stackoverflow.com/questions/588004/is-floating-point-math-broken – Code-Apprentice Jan 25 '19 at 17:58
  • Also: https://stackoverflow.com/questions/4915462/how-should-i-do-floating-point-comparison (yes, the code is in Java, but the underlying concepts are the same) – Code-Apprentice Jan 25 '19 at 17:59
  • Agreed with @Kevin. It's almost certain that you had a sequence of math operations to compute `minValue` and/or `value` that would logically arrive at `-1.234`, but in fact arrive at `-1.23400000000000000003` (for `value`) or `-1.2339999999999995` (for `minValue`) or the like. – ShadowRanger Jan 25 '19 at 18:00
  • Possible duplicate of [How should I do floating point comparison?](https://stackoverflow.com/questions/4915462/how-should-i-do-floating-point-comparison) – Code-Apprentice Jan 25 '19 at 18:01
  • I added my minValue and maxValue calculation to the OP – Max Jan 25 '19 at 18:11
  • "minValue is, however, a result of a polynomial function, but I don't believe there is any truncation or rounding there." So `minValue` is the result of a calculation. The problem here is that floating point numbers in a computer are not the same as the theoretical real numbers. We cannot represent all of the infinite real numbers with a finite number of bits. This means that we lose precision when doing calculations. So the result **is** truncated due to the inherent limitations of computers. – Code-Apprentice Jan 25 '19 at 18:17
  • @Max: Side-note: `Decimal(str(minValue))` is not going to fix much of anything. Yes, Python's `str` does avoid `Decimal`'s problem of interpreting `float`s well beyond the limits of their precision (so `minValue = -1.234`, `Decimal(str(minValue))` gets `Decimal('-1.234')`, where `Decimal(minValue)` would get `Decimal('-1.233999999999999985789145284797996...375')` or the like), but if `minValue` isn't *exactly* `-1.234` (usually the case when computed), but rather more like `-1.2339999999999998`, Python's simplification of `float` strings algorithm won't trim it, and you'll still have problems. – ShadowRanger Jan 25 '19 at 18:26

2 Answers2

1

Note that in general, this isn't going to be solvable without Decimal; floating point math is inherently imprecise across many calculations, and what should logically arrive at -1.234 can easily become -1.2339999999999998 or -1.2340000000000002 (only for illustration, your computer may differ, but those are the closest values to -1.234 on either side on my machine that don't end up rounding off to the canonical representation of -1.234; your calculation might be that close, or it might be off by a bit more, but likely still an error of less than 0.00000000001).

That said, if you happen to know, with certainty, that your values should never need more than X decimal places of precision, you can often get away with using the faster float, via the round function, to ensure your values are really the closest representable value to what they should be logically. Since it appears minValue and maxValue are computed once up front, just change the last step to round them off as well, e.g.:

minValue = round(min(y), 3)
maxValue = round(max(y), 3)

which removes any spurious excess precision. Heck, if you're not sure how precise you need to be, just double your best guess; the imprecision you're likely to see in most cases is usually well past 10 digits past the decimal point, so if you only think you need three digits, but you're worried you might still remove valid data (e.g. maybe you've got a division by 8 that might leave an extra, valid 125 at the end), just go up a bit more, rounding to 6, or even 9 decimal places.

Similarly, change the code that performs the comparison to normalize value as well, e.g.:

if round(float(value), 3) < minValue or round(float(value), 3) > maxValue:

or even better (avoids doubling up the conversion code):

if not minValue <= round(float(value), 3) <= maxValue:

which takes advantage of Python's chained comparison operators (it's equivalent to typing if not (minValue <= round(float(value), 3) and round(float(value), 3) <= maxValue):, except round(float(value), 3) is only computed once).

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • Thank you for the detailed response. I think you are right and it must be the nature of floating point calculations that is causing this issue. Rounding my min and max values to 5 decimal places fixed my problem and the comparison is working as intended. I have accepted your answer as correct. – Max Jan 25 '19 at 18:35
  • @Max: Glad it helped. If you can, please take the time to read a bit more about [IEEE 754 binary floating point](https://en.wikipedia.org/wiki/IEEE_754#Basic_and_interchange_formats) (the spec behind floating point math on most architectures and languages) and [its](https://stackoverflow.com/q/4915462/364696) [pitfalls](https://stackoverflow.com/q/588004/364696), in particular [the `binary64` format](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) (aka C's `double`) that is the underlying type of Python's `float` object. When precision matters, it always lets you down. – ShadowRanger Jan 25 '19 at 18:41
  • I appreciate the resource links, I will make sure to read up more on floating point. – Max Jan 25 '19 at 18:46
  • Just in case it ever comes up, so you know to be aware of the possibility: Not all architectures' floating point is IEEE 754 compliant. And even when it is, it might differ based on compiler settings (IEEE 754 has some strict rules on order of operations and the like that inhibit compiler optimizations, and some compilers will default to less strict rules to gain speed unless explicitly told not to). I've had a C calculation result in `16.00...001` on one machine, and `15.99...994` on another; the code being ported cast it to `int`, then multiplied; the final value was off by *100*. :-( – ShadowRanger Jan 25 '19 at 18:47
  • Wow. Being off by 100 really shows how fast this can escalate from a minor rounding inconsistency to being completely out of range. So the moral of the story is that you can never trust a floating point number hahaha – Max Jan 25 '19 at 18:55
  • @Max: Yeah. As it happened, the code only needed about two decimal places of precision, and the intermediate values never exceeded about 10000 at most. So I just changed the code so that, on entry to the function, the `double` inputs were multiplied by 10000 (to get four places of precision, just in case), added 0.5 to round, not truncate, then cast to `unsigned`. All the constants were also multiplied by 10000 and made `unsigned`. Then all the math was done with fixed point integer math (so all the values were 10000x larger) with no precision issues, and only divided back to `double` on exit. – ShadowRanger Jan 25 '19 at 19:00
  • Not only solved the problem, it reduced the code's run time by about 10% or so on most machines, and by more than 70% (I think, might have been 90%) on one particularly old machine it was ported to that didn't have a floating point coprocessor (so it used a stack based implementation based on integer math that ran *much* slower) where switching to fixed point integer math meant each operation went from half a dozen or so hardware ops to a single hardware op. – ShadowRanger Jan 25 '19 at 19:03
-2

The reason it is not working is becasue -1.234 is not greater or less than -1.234. -1.234 > 1.234 is false and -1.234 < 1.234is also false. If you want to also make it true if they are equal you would need to use => or =< which means "greater then or equal to"

>>> value=-1.234
>>> maxValue=-1.234
>>> minValue=-1.234
>>> float(value)
-1.234
>>> float(value) < minValue or float(value) > maxValue
False
>>> float(value) <= minValue or float(value) >= maxValue
True
Buzz
  • 1,877
  • 21
  • 25
  • 1
    They want the statement to be false when they're equal. That is, `value` is allowed to be the *same* as `minValue`, just not *smaller*. The test is for out of bounds conditions that trigger an error, not in bounds conditions, and the bounds are inclusive bounds, not exclusive. – ShadowRanger Jan 25 '19 at 18:10