18

I am trying to check if a number is in range of integers and returns a number based on which range it lies. I was wondering if is there a better and more efficient way of doing this:

def checkRange(number):
    if number in range(0, 5499):
        return 5000
    elif number in range(5500, 9499):
        return 10000
    elif number in range(9500, 14499):
        return 15000
    elif number in range(14500, 19499):
        return 20000
    elif number in range(19500, 24499):
        return 25000
    elif number in range(24500, 29499):
        return 30000
    elif number in range(29500, 34499):
        return 35000
    elif number in range(34500, 39499):
        return 40000
    elif number in range(39500, 44499):
        return 45000

This felt like a waste of resources and would greatly appreciate if there is a better way to do this.

Attie
  • 6,690
  • 2
  • 24
  • 34
user13539846
  • 425
  • 2
  • 5
  • 13
  • 1
    A more efficient way would be to check if the number is >= the lower limit and <= the upper limit of each range. – jkr May 31 '20 at 02:08
  • "This felt like a waste of resources and would greatly appreciate if there is a better way to do this." A waste of resources in what sense? Better in what way? Please be specific/precise – juanpa.arrivillaga May 31 '20 at 02:45
  • 1
    Your code is flawed as `checkRange(5499)` returns `None` rather than `5000`. – Tonechas May 31 '20 at 03:28

9 Answers9

27

Since you have continuous, sorted ranges, a quicker and less verbose way to do this, is to use the bisect module to find the index in a list of breakpoints and then use it to get the corresponding value from a list of values:

import bisect

break_points = [5499,  9499, 14499, 19499, 24499, 29499, 34499, 39499, 44499]
values       = [5000, 10000, 15000, 20000, 25000, 30000, 35000, 40000, 45000]

n = 10000
index = bisect.bisect_left(break_points, n)

values[index]
# 15000

You'll need to test for n values that exceed the last breakpoint if that's a possibility. Alternatively you can add a default value to the end of the values list.

Attie
  • 6,690
  • 2
  • 24
  • 34
Mark
  • 90,562
  • 7
  • 108
  • 148
  • 3
    Now this actually is algorithmically faster (as long as you aren't creating the lists on each method call ..)! However, for something this small I bet the linear search would win, unless the lists got bigger – juanpa.arrivillaga May 31 '20 at 02:44
  • 1
    Oh wow, this is great since im planning to add more conditional statement without adding too many lines into the code. Thank you for this new approach! – user13539846 May 31 '20 at 02:46
  • @juanpa.arrivillaga you may be right, but `bisect` is pretty blazing fast. I don't want to clog up this thread with talk of nanoseconds. However, I have not been able to make a linear search that's faster even for short lists. – Mark May 31 '20 at 04:24
6

If by better, you mean faster, it's a good idea to check the lower and upper limits, as the previous comments/answers have suggested.

However, note that in Python3, the range() object does this for you, resulting in the in range() check being a nearly constant time operation, so I don't think the runtime should be bad using your code.

I'd highly recommend reading this thread:

Why is "1000000000000000 in range(1000000000000001)" so fast in Python 3?

adamgy
  • 4,543
  • 3
  • 16
  • 31
4

If there are many ranges your solution becomes very verbose. You may want to give this (simpler) code a try:

limits = (0, 5500, 9500, 14500, 19500, 24500, 29500, 34500, 39500, 44500)

def checkRange(number):
    for i, (low, high) in enumerate(zip(limits[:-1], limits[1:]), 1):
        if low <= number < high:
            return 5000*i
    return 0  # when the passed number is out of range

It is worth pointing out that your logic has an off-by-one error, whereas in my implementation this issue is fixed.

Demo:

In [188]: checkRange(5499)
Out[188]: 5000

In [189]: checkRange(5500)
Out[189]: 10000

In [190]: checkRange(24872)
Out[190]: 30000
Tonechas
  • 13,398
  • 16
  • 46
  • 80
1

If your ranges are set in stone you could loop through a list of your ranges:

ranges = [[0,5499],[5500,9499],[9500,14499],[14500,19499],[19500,24499],[24500,29499],[29500,34499],[34500,39499],[39500,44499]]
returns = [5000,10000,15000,20000,25000,30000,35000,40000,45000]

def checkRange(number):
    for i in range(len(returns)):
        if number in range(ranges[i][0], ranges[i][1]):
            return returns[i]

# Test a few values:
print(checkRange(10))
print(checkRange(6000))

I get output:

5000 
10000

Also, be sure to fix your 8th entry so that it is a range and not a single int.

1

If your goal is really to do this kind of "rounding up" operation, it might be the fastest to just use math:

def round_up_to(num, factor):
    return -(-num // factor ) * factor

def checkRange2(num):
    if num < 5500:  # was this first interval perhaps a mistake?
        return 5000
    else:
        return round_up_to(num + 501, 5000))

This will work for arbitrarily large integers.

chthonicdaemon
  • 19,180
  • 2
  • 52
  • 66
0

If the logic needs to be generalized then, some form of following function depending on the rounding off and range check can be used:

def checkRange(number):

   count = 0
   while int(number):
      number /= 10
      count += 1

   round_off = count - 1
   for i in range(count - round_off):
      number *= 10

   number = int(number)

   return int((10**(count - 1)) * (number))

So basically, find number of digit the number have then count number of digits you want to preserve and generate the rounded number as a function of these two. Parameratization of the round off can be done as well as needed. We can also include offset as a function of number of digits minus required round off's.

Also from sample code, the lower bound is not required with method even further simplifying the code.

sbh
  • 407
  • 4
  • 13
0

First of all, let me clear the range function. It works excluding the ending limit. So the if condition you are using with range(0,5499) will check for the number in this list [0, 1, ..... , 5477, 5488] not this list [0, 1, ..... , 5477, 5488, 5499].

Now to answer your question, there can be multiple efficient ways to do the job(depending upon the end result) so, the bisect package mentioned as the accepted answer does the job pretty well. In case you want to do it in an efficient way without any library, below code should help:

rangeList =  [5499, 9499, 14499, 19499, 24499, 29499, 34499, 39499, 44499]
returnList = [5000, 10000, 15000, 20000, 25000, 30000, 35000, 40000, 45000]

def checkRange(number):
    for i in range(len(rangeList)):
        if number<=rangeList[i]:
            return returnList[i]
    return -1

inp: checkRange(5499)
op: 5000

This code works efficiently using basic principles of if condition and considering that your ranges are in ascending order.

Desmond
  • 405
  • 1
  • 6
  • 12
0

I tend to write these type of conditionals into a mathematical formula as

return (aFunction((number - Min)/intervalLength) + c1) * c2

where c1, c2 and whether aFunction is ceil or floor depend on what you want and the way you treat boundaries.

Here, apart from the first two intervals, all the other intervals are of length 5000. So I would write the formula for the rest. So,

def checkRange(number):
    if number in range(0, 5499):
        return 5000
    elif number in range(5500, 9499):
        return 10000
    elif number in range(9500, 44499):
        return (floor((number - 9500)/4000) + 3) * 5000

Although, I have to admit this solution is elegant only if your intervals are all the same with similar boundary conditions and your result has the same incrememt from one condition to the next.

Masoud Ghaderi
  • 437
  • 5
  • 16
-1

you should replace:

if number in range(0,5499):

with

if 0 <= number < 5499 :

which is much faster and less memory intensive.

lenik
  • 23,228
  • 4
  • 34
  • 43
  • 4
    No, it isn't much less memory intensive. `range` objects use a small, constant memory overhead and has constant time membership testing – juanpa.arrivillaga May 31 '20 at 02:28
  • @juanpa.arrivillaga have you ever heard about python2 ? – lenik May 31 '20 at 02:30
  • 6
    Yes. Python 2 is well passed it's end of life. On the Python tag, it is generally assumed you mean python 3 unless explicitly noted otherwise. In *any case* the question is tagged Python 3 – juanpa.arrivillaga May 31 '20 at 02:31
  • @juanpa.arrivillaga optimizations introduced in the latest versions are not an excuse to write bad code – lenik May 31 '20 at 03:10
  • 8
    It's not an optimization, it's *part of the new python spec*. And it's not the latest version, Python 3 came out over a decade ago and Python 2 *is no longer supported*. `range` in python 3 returns a `range` object, not a list, so this is a perfectly reasonable use of a range object, this is what they are for. It's not "bad code" it's using the built in data structures to their intended purpose – juanpa.arrivillaga May 31 '20 at 03:15