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.

3
  • 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, 2020 at 2: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 May 31, 2020 at 2:45
  • 1
    Your code is flawed as checkRange(5499) returns None rather than 5000.
    – Tonechas
    May 31, 2020 at 3:28

9 Answers 9

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.

3
  • 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 May 31, 2020 at 2: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! May 31, 2020 at 2: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, 2020 at 4: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?

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
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.

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.

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.

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.

-1

you should replace:

if number in range(0,5499):

with

if 0 <= number < 5499 :

which is much faster and less memory intensive.

5
  • 4
    No, it isn't much less memory intensive. range objects use a small, constant memory overhead and has constant time membership testing May 31, 2020 at 2:28
  • @juanpa.arrivillaga have you ever heard about python2 ?
    – lenik
    May 31, 2020 at 2: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 May 31, 2020 at 2:31
  • @juanpa.arrivillaga optimizations introduced in the latest versions are not an excuse to write bad code
    – lenik
    May 31, 2020 at 3: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 May 31, 2020 at 3:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.