DEV Community

Cover image for Day 34: Python Balanced Numbers Filter, Identify Numbers with Equal Even and Odd Digits Using Modular Checks
Shahrouz Nikseresht
Shahrouz Nikseresht

Posted on

Day 34: Python Balanced Numbers Filter, Identify Numbers with Equal Even and Odd Digits Using Modular Checks

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.
    """
Enter fullscreen mode Exit fullscreen mode

Initialize counters:

even = 0
odd = 0
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Return the tuple:

return even, odd
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)