Welcome to Day 34 of the #80DaysOfChallenges journey! This intermediate-level challenge dives deeper into filtering a list for 'balanced' numbers, where a number is considered balanced if it has an equal count of even and odd digits, regardless of its sign. By breaking it down into helper functions for digit counting and balance checking, plus input validation, this exercise emphasizes modular code design, efficient string handling for digits, and robust error management. It's an excellent step up for practicing type checks, iteration over strings, and arithmetic ops like modulo, which are crucial in data validation or numeric analysis tasks. If you're transitioning from beginner loops to more structured functions or want to refine your approach to edge cases like negatives and non-integers, this "Python balanced numbers" guide explores a versatile filter that's easy to adapt for similar digit-based problems, such as lucky numbers or digit sums.
💡 Key Takeaways from Day 34: Balanced Numbers Filtering System
This challenge constructs a system of functions to process any iterable of numbers, validate them as integers, count even/odd digits (ignoring signs), check for balance, and collect the balanced ones. It's a model of clean, reusable code: separate concerns into small functions, handle errors early, and optimize where practical. We'll dissect it in detail: digit counting with efficient conversion, balance check via tuple unpack, filtering with validation and iteration, plus insights on why certain techniques were chosen for performance and readability.
1. Digit Counting: Handling Signs and Fast Conversion
The count_even_odd_digits function processes a single number, returning counts as a tuple. Its signature uses typing for clarity:
def count_even_odd_digits(num: int) -> tuple[int, int]:
"""
Return a tuple (even_count, odd_count) for the given integer.
Handles negative numbers by ignoring the minus sign.
"""
Initialize counters:
even = 0
odd = 0
Convert to string and iterate:
for digit in str(abs(num)): # abs to ignore negative sign
d = ord(digit) - 48 # faster than int(digit), still readable
if d % 2 == 0:
even += 1
else:
odd += 1
Return the tuple:
return even, odd
Here, abs(num) strips the sign, ensuring -78 is treated like 78 (one even, one odd). The ord(digit) - 48 trick converts char digits to ints quicker than int(digit), leveraging ASCII values ('0' is 48), which is a micro-optimization useful in large-scale processing but keeps code readable. This approach avoids lists or comprehensions for simplicity, focusing on a basic loop that's easy to debug. It's efficient for typical numbers (up to ~20 digits) and teaches string-as-iterable, a common Python idiom for numeric breakdowns.
2. Balance Check: Simple Equality Test
The is_balanced_number function uses the counter to decide balance:
def is_balanced_number(num: int) -> bool:
"""
Return True if even digit count equals odd digit count.
"""
# Count the number of even and odd digits in `num`
even_count, odd_count = count_even_odd_digits(num)
# Return True if the number of even digits equals the number of odd digits
return even_count == odd_count
Tuple unpacking makes it concise. This separation allows testing the balance logic independently, promoting modularity. For example, 1234 (digits:1-odd,2-even,3-odd,4-even) has 2 even/2 odd, so True. It handles 0 (no digits? Wait, str(0)='0', even:1, odd:0, False) or singles correctly. By reusing the counter, it avoids redundant code, adhering to DRY principles, and could extend to other checks like even-dominant.
3. Filtering Logic: Validation and Collection
The main filter_balanced_numbers processes an iterable, checks types, and filters:
def filter_balanced_numbers(numbers: Iterable[int]) -> List[int]:
"""
Return a list of balanced numbers from any iterable of integers.
Raises a ValueError if elements are not integers.
"""
result = [] # Empty list to store numbers that are balanced
for n in numbers: # Iterate over each element in the input list
if not isinstance(n, int): # If the element is not an integer, it's invalid
raise ValueError(f"Invalid element detected: {n} (expected int)") # Raise an error for invalid input types
if is_balanced_number(n): # Check whether the current number is balanced
result.append(n) # If balanced, add it to the result list
return result # Return the list of all balanced numbers found
Iterable[int] allows lists, tuples, etc., for flexibility. The isinstance check halts on non-ints, preventing subtle bugs (e.g., floats like 1.5 would str to '1.5', messing digits). Raising ValueError with the offender aids debugging. The append-only loop is straightforward, preserving order, and efficient for small inputs. In the example:
if __name__ == "__main__":
nums = [1234, 550, -78, 6, 2024, 51, 1314, -909, 33, 2023]
balanced = filter_balanced_numbers(nums)
print("\n🎯 Balanced numbers:", balanced, "\n")
It filters to [1234, -78, 2024, 1314, -909], as these have equal even/odd digits. This demo shows real-world use, and you could add try-except in callers for graceful handling.
🎯 Summary and Reflections
This balanced numbers filter showcases how modular functions create robust, testable code for numeric tasks. It deepened my appreciation for:
- Modularity benefits: Helpers like count_even_odd_digits isolate logic, making extensions (e.g., include 'y' as vowel? Wait, digits only) simple without rewriting.
- Performance tweaks: ord - 48 vs int(), minor, but illustrates thinking about micro-effs in loops, especially for big data.
- Validation importance: Early isinstance prevents garbage-in-garbage-out, a must in user-input or API scenarios.
- Edge case coverage: abs for negatives ensures sign-agnostic, reflecting real needs like check digits in IDs.
- Typing and docs: From typing import adds clarity, aiding IDEs and readers.
What surprised me was how digit strings enable quick analysis without math division/modulo loops (which could handle larger nums but complicate). For improvements, consider zero-digit nums (like 0: even=1? Debate if '0' counts). Overall, it's a bridge to advanced topics like regex for digits or stats on distributions.
Advanced Alternatives: Use collections.Counter on str(abs(num)) for counts, or lambda in filter: list(filter(lambda n: (e:=sum(1 for d in str(abs(n)) if int(d)%2==0)) == len(str(abs(n)))-e, numbers)). For speed, bit ops on d. How do you analyze digits? Drop ideas!
🚀 Next Steps and Resources
Day 34 elevated numeric filtering with modularity, priming for pattern-matching challenges. In #80DaysOfChallenges? Added sum check? Share your mod!
- Source Code for Challenge #34: scripts/filter_balanced_numbers.py
- Main Repository: 80-days-of-challenges
- Daily Updates: Twitter/X (@Shahrouzlogs)
Top comments (0)