Home > OS >  How to compare if a floating point number against a set of floating point ranges?
How to compare if a floating point number against a set of floating point ranges?

Time:03-29

Example, I have 4 ranges:

  1. 0 - 1.25
  2. 1.26 - 2.45
  3. 2.46 - 5
  4. 5.01 - infinity

And I have a floating point number to compare with: 1.2549999999.

I need to check to what range this number belongs.

I have the following code, but I'm not convinced it's efficient enough


$comparedNumber = 1.2549999999;

if (0 < $comparedNumber && round($comparedNumber, 2) <= round(1.25,2)) {
    $selectedRange = 'Range 1';
} elseif (  round(1.26,2) <= round($comparedNumber, 2)  && round($comparedNumber, 2) <= round(2.45,2)) {
    $selectedRange = 'Range 2';
} elseif (  round(2.46,2) <= round($comparedNumber, 2)  && round($comparedNumber, 2) <= round(5,2)) {
    $selectedRange = 'Range 3';
} elseif ( round(5.01,2) <= round($comparedNumber, 2) ) {
    $selectedRange = 'Range 4';
} else {
    $selectedRange = 'Range not exist';
}

print_r($selectedRange);

Sample here

CodePudding user response:

Your current ranges could potentially have gaps: 1.250001 would not be <= 1.25, but would not be >= 1.26 either. You've tried to handle that with round(), but the result of that is still a floating point number, and binary floating point does not accurately represent decimals. This isn't unique to PHP, it's something you'll encounter in basically every programming language (a very few have a separate type for fixed decimal numbers, trading performance for accuracy).

In particular, writing round(1.25, 2) is never going to result in a different value from writing 1.25, because the compiler will already have picked the closest floating point value to 1.25 possible.

The simple fix is to use the same boundary each time, but exclude equal values on second mention: rather than >= 1.26 in the second range, use > 1.25. However, that makes it obvious that you have redundant tests anyway, because if something doesn't fall into the <= 1.25 bucket, you already know that it is > 1.25, so don't need to test that again.

For readability (and an immeasurably tiny amount of performance), I would assign a local variable to round($comparedNumber, 2) rather than pasting it into each check. You may also decide you don't want that rounding - its effect will be to put 1.251 into the ">0, <=1.25" bucket rather than the ">1.25, <=2.45" bucket.

So it simplifies down to this:

$comparedNumber = 1.2549999999;
$roundedNumber = round($comparedNumber, 2);

if ($roundedNumber <= 0) {
    $selectedRange = 'Range not exist';
} elseif ($roundedNumber <= 1.25) {
    $selectedRange = 'Range 1';
} elseif ($roundedNumber <= 2.45) {
    $selectedRange = 'Range 2';
} elseif ($roundedNumber <= 5) {
    $selectedRange = 'Range 3';
} else {
    $selectedRange = 'Range 4';
}

Since you now only need one number to define each range, making this into a loop is simple:

foreach  ( $ranges as $rangeName => $rangeBoundary ) {
   if ( $roundedNumber <= $rangeBoundary ) {
      $selectedRange = $rangeName;
      break; // stops the loop carrying on with the next test
   }
}

CodePudding user response:

Your problem is poorly thought out boundaries, and trying to use equality to compare floating points. Rounding is not a solution: the return value of round() is still a floating point number.

For your "ranges" you have actually three boundaries: 1.26, 2.46, and 5.01.

A generalized solution would be:

<?php

$numbers    = [1.2549999999, 1.28012, 2.01212, 4.012, 5.0000012, 5.012121001, -0.12];
$boundaries = [1.26, 2.46, 5.01];

function checkRange(float $number, array $boundaries): int {

    if ($number < 0) {
      return -1;
    }

    foreach ($boundaries as $i => $boundary) {
        if ($number < $boundary) {
            return $i   1;
            break;
        }
    }
    return 4;
}

foreach ($numbers as $number) {
    echo "$number at Range ", checkRange($number, $boundaries), "\n";
}

/*
Output:
1.2549999999 at Range 1
1.28012 at Range 2
2.01212 at Range 2
4.012 at Range 3
5.0000012 at Range 3
5.012121001 at Range 4
-0.12 at Range -1
*/

As seen working here.

Note that the solution in the other answer fails to account for numbers in the range 4.

For this exercise, I treat numbers below 0 as "out of range", and put them in "range -1". What to do exactly with those is up to you.

This works for any given set of boundaries (as long as they are ordered), and does not need rounding at any point, since it's moot for the comparison. A number is less than the boundary, or it's not.

CodePudding user response:

In addition to other good answers discussing the weak idea of rounding:

Performance question too, maybe there are built in function or some trick

If the number of ranges was large, like 10 , code could efficiently determine the range via a binary search on the list of limits. With 10 limits, this would take at most 4 iterations O(log n),rather than 10 (O(n)). With 100 limits, takes at most 7.

If the ranges were approximately linearly distributed, an average range look would be O(1).

With real life distributions, a combination of the above 2 strategies is best.

With a fixed group of 4, simply test against the middle one, and then the remaining quarter.

  • Related