I normally implement switch/case for equal comparison using a dictionary.
dict = {0:'zero', 1:'one', 2:'two'};
a=1; res = dict[a]
instead of
if a == 0:
res = 'zero'
elif a == 1:
res = 'one'
elif a == 2:
res = 'two'
Is there a strategy to implement similar approach for non-equal comparison?
if score <= 10:
cat = 'A'
elif score > 10 and score <= 30:
cat = 'B'
elif score > 30 and score <= 50:
cat = 'C'
elif score > 50 and score <= 90:
cat = 'D'
else:
cat = 'E'
I know that may be tricky with the <, <=, >, >=, but is there any strategy to generalize that or generate automatic statements from let's say a list
{[10]:'A', [10,30]:'B', [30,50]:'C',[50,90]:'D',[90]:'E'}
and some flag to say if it's <
or <=
.
A dictionary can hold a lot of values. If your ranges aren't too broad, you could make a dictionary that is similar to the one you had for the equality conditions by expanding each range programmatically:
from collections import defaultdict
ranges = {(0,10):'A', (10,30):'B', (30,50):'C',(50,90):'D'}
valueMap = defaultdict(lambda:'E')
for r,letter in ranges.items():
valueMap.update({ v:letter for v in range(*r) })
valueMap[701] # 'E'
valueMap[7] # 'A'
You could also just remove the redundant conditions from your if/elif statement and format it a little differently. That would almost look like a case statement:
if score < 10 : cat = 'A'
elif score < 30 : cat = 'B'
elif score < 50 : cat = 'C'
elif score < 90 : cat = 'D'
else : cat = 'E'
To avoid repeating score <
, you could define a case function and use it with the value:
score = 43
case = lambda x: score < x
if case(10): cat = "A"
elif case(30): cat = "B"
elif case(50): cat = "C"
elif case(90): cat = "D"
else : cat = "E"
print (cat) # 'C'
You could generalize this by creating a switch function that returns a "case" function that applies to the test value with a generic comparison pattern:
def switch(value):
def case(check,lessThan=None):
if lessThan is not None:
return (check is None or check <= value) and value < lessThan
if type(value) == type(check): return value == check
if isinstance(value,type(case)): return check(value)
return value in check
return case
This generic version allows all sorts of combinations:
score = 35
case = switch(score)
if case(0,10) : cat = "A"
elif case([10,11,12,13,14,15,16,17,18,19]):
cat = "B"
elif score < 30 : cat = "B"
elif case(30) \
or case(range(31,50)) : cat = 'C'
elif case(50,90) : cat = 'D'
else : cat = "E"
print(cat) # 'C'
And there is yet another way using a lambda function when all you need to do is return a value:
score = 41
case = lambda x,v: v if score<x else None
cat = case(10,'A') or case(20,'B') or case(30,'C') or case(50,'D') or 'E'
print(cat) # "D"
This last one can also be expressed using a list comprehension and a mapping table:
mapping = [(10,'A'),(30,'B'),(50,'C'),(90,'D')]
scoreCat = lambda s: next( (L for x,L in mapping if s<x),"E" )
score = 37
cat = scoreCat(score)
print(cat) #"D"
More specifically to the question, a generalized solution can be created using a setup function that returns a mapping function in accordance with your parameters:
def rangeMap(*breaks,inclusive=False):
default = breaks[-1] if len(breaks)&1 else None
breaks = list(zip(breaks[::2],breaks[1::2]))
def mapValueLT(value):
return next( (tag for tag,bound in breaks if value<bound), default)
def mapValueLE(value):
return next( (tag for tag,bound in breaks if value<=bound), default)
return mapValueLE if inclusive else mapValueLT
scoreToCategory = rangeMap('A',10,'B',30,'C',50,'D',90,'E')
print(scoreToCategory(53)) # D
print(scoreToCategory(30)) # C
scoreToCategoryLE = rangeMap('A',10,'B',30,'C',50,'D',90,'E',inclusive=True)
print(scoreToCategoryLE(30)) # B
Note that with a little more work you can improve the performance of the returned function using the bisect module.
The bisect
module can be used for such categorization problem. In particular, the documentation offers an example which solves a problem very similar to yours.
Here is the same example adapted to your use case. The function returns two values: the letter grade and a bool
flag which indicates if the match was exact.
from bisect import bisect_left
grades = "ABCDE"
breakpoints = [10, 30, 50, 90, 100]
def grade(score):
index = bisect_left(breakpoints, score)
exact = score == breakpoints[index]
grade = grades[index]
return grade, exact
grade(10) # 'A', True
grade(15) # 'B', False
In the above, I assumed that your last breakpoint was 100
for E
. If you truly do not want an upper bound, notice that you can replace 100
by math.inf
to keep the code working.
For your particular case an efficient approach to convert a score to a grade in O(1) time complexity would be to use 100 minus the score divided by 10 as a string index to obtain the letter grade:
def get_grade(score):
return 'EDDDDCCBBAA'[(100 - score) // 10]
so that:
print(get_grade(100))
print(get_grade(91))
print(get_grade(90))
print(get_grade(50))
print(get_grade(30))
print(get_grade(10))
print(get_grade(0))
outputs:
E
E
D
C
B
A
A
Python 3.10 introduced match
case (basically switch
) and you can use it as
def check_number(no):
match no:
case 0:
return 'zero
case 1:
return 'one'
case 2:
return 'two'
case _:
return "Invalid num"
This is something that I tried for an example.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With