25

I'm refactoring a function that, given a series of endpoints that implicitly define intervals, checks if a number is included in the interval, and then return a corresponding (not related in any computable way). The code that is now handling the work is:

if p <= 100:
    return 0
elif p > 100 and p <= 300:
    return 1
elif p > 300 and p <= 500:
    return 2
elif p > 500 and p <= 800:
    return 3
elif p > 800 and p <= 1000:
    return 4
elif p > 1000:
    return 5

Which is IMO quite horrible, and lacks in that both the intervals and the return values are hardcoded. Any use of any data structure is of course possible.

Agos
  • 18,542
  • 11
  • 56
  • 70

6 Answers6

55
import bisect
bisect.bisect_left([100,300,500,800,1000], p)

here the docs: bisect

Brown Bear
  • 19,655
  • 10
  • 58
  • 76
Glenn Maynard
  • 55,829
  • 10
  • 121
  • 131
  • 3
    Truly impressive. Super clean, and I believe very fast too. It can also be easily extended in case one does need a non-natural ordering or something else in return, like a string: import bisect n = bisect.bisect_left([100,300,500,800,1000], p) a=["absent","low","average","high", "very high", "extreme"] a[n] – Agos Jul 29 '09 at 19:55
  • hmmm but this doesn't return "arbitrary values" it returns the index. How do I have it return the arbitrary value? I tried `p = 10 x = bisect.bisect_left(OrderedDict({10: 'a', 11: 'b'}), p) print() ` but it didn't work. – Charlie Parker Mar 16 '21 at 17:41
3

You could try a take on this:

def check_mapping(p):
    mapping = [(100, 0), (300, 1), (500, 2)] # Add all your values and returns here

    for check, value in mapping:
        if p <= check:
            return value

print check_mapping(12)
print check_mapping(101)
print check_mapping(303)

produces:

0
1
2

As always in Python, there will be any better ways to do it.

kjfletch
  • 5,394
  • 3
  • 32
  • 38
3

It is indeed quite horrible. Without a requirement to have no hardcoding, it should have been written like this:

if p <= 100:
    return 0
elif p <= 300:
    return 1
elif p <= 500:
    return 2
elif p <= 800:
    return 3
elif p <= 1000:
    return 4
else:
    return 5

Here are examples of creating a lookup function, both linear and using binary search, with the no-hardcodings requirement fulfilled, and a couple of sanity checks on the two tables:

def make_linear_lookup(keys, values):
    assert sorted(keys) == keys
    assert len(values) == len(keys) + 1
    def f(query):
        return values[sum(1 for key in keys if query > key)]
    return f

import bisect
def make_bisect_lookup(keys, values):
    assert sorted(keys) == keys
    assert len(values) == len(keys) + 1
    def f(query):
        return values[bisect.bisect_left(keys, query)]
    return f
John Machin
  • 81,303
  • 11
  • 141
  • 189
  • I like this one better than the one that has the most votes because of its more generalized/non-hardcoded form and because it is more in-depth. – JAB Jul 29 '09 at 12:57
0

Try something along the lines of:

d = {(None,100): 0, 
    (100,200): 1,
    ...
    (1000, None): 5}
value = 300 # example value
for k,v in d.items():
    if (k[0] is None or value > k[0]) and (k[1] is None or value <= k[1]):
        return v
stefanw
  • 10,456
  • 3
  • 36
  • 34
0
def which_interval(endpoints, number):
    for n, endpoint in enumerate(endpoints):
        if number <= endpoint:
            return n
        previous = endpoint
    return n + 1

Pass your endpoints as a list in endpoints, like this:

which_interval([100, 300, 500, 800, 1000], 5)

Edit:

The above is a linear search. Glenn Maynard's answer will have better performance, since it uses a bisection algorithm.

Steef
  • 33,059
  • 4
  • 45
  • 36
  • Lose the "previous" caper; it's quite redundant. – John Machin Jul 29 '09 at 12:58
  • Yeah, you're right, I guess the original code "inspired" me to use it. BTW, your use of the imperative might sound a bit gruff to some. – Steef Jul 29 '09 at 13:42
  • @Steef: You may wish to consider a humble suggestion that you might at your leisure revist your answer, note that **your answer still includes a redundant line of code**, and in the fullness of time, excise the same. – John Machin Aug 01 '09 at 00:02
0

Another way ...

def which(lst, p): 
    return len([1 for el in lst if p > el])

lst = [100, 300, 500, 800, 1000]
which(lst, 2)
which(lst, 101)
which(lst, 1001)
ars
  • 120,335
  • 23
  • 147
  • 134