0

I have a dictionary:

BATTERY_LEVEL_TRANSFORMS = {(75, 101): "High", (30, 75): "Medium", (0, 30): "Low"}

and am trying to set a text indicator based on a battery value. Whichever key it is in the range of, the text will be assigned accordingly. Here's what I have:

 for level_range, level_text in BATTERY_LEVEL_TRANSFORMS.items():
 if msg in range(*level_range):
 batt_level_str = level_text
 break
 else:
 batt_level_str = "Error"

That's adjusted code to make the problem understandable. Is this the proper way to do this? It doesn't seem to be the correct solution, but I can't think of what the correct solution would be (other than the equivalent interval conditionals).

asked Aug 20, 2016 at 5:35
3
  • If it's possible I would reverse that dictionary. Those keys should be the values. Commented Aug 20, 2016 at 5:40
  • Why do you think it's incorrect? What is your expected output? Commented Aug 20, 2016 at 5:54
  • 1
    It works, it just doesn't seem idiomatic. I feel like there is a built in function I'm not using that I should be. Maybe something with sets, I don't know. Commented Aug 20, 2016 at 5:55

2 Answers 2

2

One option would be store the breakpoints between levels and level names to two sorted lists and use bisect.bisect_right to do binary search on the breakpoints. Benefit of this approach would be that level retrieval would be O(log n) time complexity although it doesn't really matter when you have only couple levels:

from bisect import bisect_right
LEVELS = [0, 30, 75, 101]
TEXTS = ['Low', 'Medium', 'High']
def get_level(num):
 index = bisect_right(LEVELS, num) - 1
 return TEXTS[index] if 0 <= index < len(TEXTS) else 'Error'
for x in [-1, 0, 29, 30, 74, 75, 100, 101]:
 print('{}: {}'.format(x, get_level(x)))

Output:

-1: Error
0: Low
29: Low
30: Medium
74: Medium
75: High
100: High
101: Error

If you need fast retrieval and are willing to utilize more space you could create a dict containing all the valid values so that retrieval would be O(1) time complexity:

BATTERY_LEVEL_TRANSFORMS = {(75, 101): "High", (30, 75): "Medium", (0, 30): "Low"}
LEVEL_MAP = {i: level
 for (lo, hi), level in sorted(BATTERY_LEVEL_TRANSFORMS.items())
 for i in range(lo, hi)}
def get_level(num):
 return LEVEL_MAP.get(num, 'Error')
for x in [-1, 0, 29, 30, 74, 75, 100, 101]:
 print('{}: {}'.format(x, get_level(x)))

Output:

-1: Error
0: Low
29: Low
30: Medium
74: Medium
75: High
100: High
101: Error

Feasibility of above approach obviously depends on how many valid values you have.

answered Aug 20, 2016 at 6:15
0

I actually think your solution is not bad. Here's my variation on it to make it a bit more readable:

BATTERY_LEVELS = (
 (0, 30, 'Low'),
 (30, 75, 'Medium'),
 (75, 101, 'High'),
)
def get_level_text(level):
 for low, high, level_text in BATTERY_LEVELS:
 if low <= level <= high:
 return level_text
 return 'Error'
print(get_level_text(20)) # Low
print(get_level_text(40)) # Medium
print(get_level_text(80)) # High
print(get_level_text(120)) # Error

Here are the changes and reasoning behind them:

  1. Change BATTERY_LEVEL_TRANSFORMS, which is a dictionary of tuples to text, to just a tuple of tuples called BATTERY_LEVELS. Since this structure doesn't change and tuples are immutable, it seems like a simplification. There's also no need to have a dictionary since we just have bunch of associated data with no clear key.
  2. Change msg to level since it seems to be a numeric value.
  3. Now that we have the tuple of tuples, change the loop to have intuitive variable names by using low, high, level_text to store the tuple values.
  4. Change the condition to if low <= level <= high to check the range in a more readable way.
answered Aug 20, 2016 at 5:58
3
  • I like that. The level variable was msg because in the actual code, it was a message sent with a pubsub framework. I like the idea of the tuple of tuples rather than dictionary, but I think I'll stick with "in range" because, to my eye, that's more readable than interval conditions. Most people know what you mean when you ask if something is "in range". Thanks! Commented Aug 21, 2016 at 3:46
  • Sure :) One caveat is that range(low, high) will generate every integer between low and high and linearly step through them until it finds the target, which is less efficient than a number comparison. So in a way, though in range(...) does what you want it to, it does it in a sort of unexpected way. Another caveat is that this also won't work for floating point numbers ;) Commented Aug 21, 2016 at 4:50
  • 1
    That's a good point. The floats aren't a concern, but if interval comparison is that much more efficient, I might change it. I was under the impression that python3 ranges were more efficient at that sort of thing (Python 2's xrange) in that they didn't generate an entire list, just an iterator. Ahh, I think I just answered my own question. An iterator can't really compare its values without iterating. Commented Aug 21, 2016 at 4:55

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.