Skip to content

Add improved doctrings and doctests for math/perfect_number.py #12830

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
322 changes: 270 additions & 52 deletions maths/perfect_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
== Perfect Number ==
In number theory, a perfect number is a positive integer that is equal to the sum of
its positive divisors, excluding the number itself.

For example: 6 ==> divisors[1, 2, 3, 6]
Excluding 6, the sum(divisors) is 1 + 2 + 3 = 6
So, 6 is a Perfect Number

Other examples of Perfect Numbers: 28, 486, ...
The first few perfect numbers are: 6, 28, 496, 8128, 33550336, ...

https://en.wikipedia.org/wiki/Perfect_number
https://oeis.org/A000396
"""


Expand All @@ -17,70 +19,286 @@ def perfect(number: int) -> bool:
Check if a number is a perfect number.

A perfect number is a positive integer that is equal to the sum of its proper
divisors (excluding itself).
divisors (positive divisors excluding the number itself).

The algorithm finds all divisors up to number//2 (since no proper divisor
can be greater than half the number) and sums them for comparison.

Time Complexity: O(sqrt(n)) with optimized divisor finding
Space Complexity: O(1)

Args:
number: The number to be checked.
number: The positive integer to be checked.

Returns:
True if the number is a perfect number otherwise, False.
Start from 1 because dividing by 0 will raise ZeroDivisionError.
A number at most can be divisible by the half of the number except the number
itself. For example, 6 is at most can be divisible by 3 except by 6 itself.
True if the number is a perfect number, False otherwise.

Raises:
ValueError: If number is not an integer.

Examples:
>>> perfect(27)
False
>>> perfect(28)
True
>>> perfect(29)
False
>>> perfect(6)
True
>>> perfect(12)
False
>>> perfect(496)
True
>>> perfect(8128)
True
>>> perfect(0)
False
>>> perfect(-1)
False
>>> perfect(33550336) # Large perfect number
True
>>> perfect(33550337) # Just above a large perfect number
False
>>> perfect(1) # Edge case: 1 is not a perfect number
False
>>> perfect("123") # String representation of a number
Traceback (most recent call last):
...
ValueError: number must be an integer
>>> perfect(12.34)
Traceback (most recent call last):
...
ValueError: number must be an integer
>>> perfect("Hello")
Traceback (most recent call last):
...
ValueError: number must be an integer
Basic perfect numbers:
>>> perfect(6)
True
>>> perfect(28)
True
>>> perfect(496)
True
>>> perfect(8128)
True

Large perfect number:
>>> perfect(33550336)
True

Non-perfect numbers:
>>> perfect(12)
False
>>> perfect(27)
False
>>> perfect(29)
False
>>> perfect(100)
False

Edge cases:
>>> perfect(1)
False
>>> perfect(2)
False
>>> perfect(0)
False
>>> perfect(-1)
False
>>> perfect(-6)
False

Numbers close to perfect numbers:
>>> perfect(5)
False
>>> perfect(7)
False
>>> perfect(27)
False
>>> perfect(29)
False
>>> perfect(495)
False
>>> perfect(497)
False
>>> perfect(33550335)
False
>>> perfect(33550337)
False

Type validation:
>>> perfect(12.34)
Traceback (most recent call last):
...
ValueError: number must be an integer
>>> perfect("123")
Traceback (most recent call last):
...
ValueError: number must be an integer
>>> perfect("Hello")
Traceback (most recent call last):
...
ValueError: number must be an integer
>>> perfect([6])
Traceback (most recent call last):
...
ValueError: number must be an integer
>>> perfect(None)
Traceback (most recent call last):
...
ValueError: number must be an integer

Testing divisor sum calculation for known cases:
>>> # For 6: divisors are 1, 2, 3 -> sum = 6
>>> sum(i for i in range(1, 6//2 + 1) if 6 % i == 0) == 6
True
>>> # For 28: divisors are 1, 2, 4, 7, 14 -> sum = 28
>>> sum(i for i in range(1, 28//2 + 1) if 28 % i == 0) == 28
True
>>> # For 12: divisors are 1, 2, 3, 4, 6 -> sum = 16 ≠ 12
>>> sum(i for i in range(1, 12//2 + 1) if 12 % i == 0) == 12
False
"""
if not isinstance(number, int):
raise ValueError("number must be an integer")

if number <= 0:
return False
return sum(i for i in range(1, number // 2 + 1) if number % i == 0) == number

# Special case: 1 has no proper divisors
if number == 1:
return False

# Find sum of all proper divisors
# We only need to check up to number//2 since no proper divisor
# can be greater than half the number
divisor_sum = sum(i for i in range(1, number // 2 + 1) if number % i == 0)

return divisor_sum == number


def perfect_optimized(number: int) -> bool:
"""
Optimized version of perfect number checker using mathematical properties.

This version uses the fact that divisors come in pairs (d, n/d) to reduce
the search space to sqrt(n).

Time Complexity: O(sqrt(n))
Space Complexity: O(1)

Args:
number: The positive integer to be checked.

Returns:
True if the number is a perfect number, False otherwise.

Examples:
>>> perfect_optimized(6)
True
>>> perfect_optimized(28)
True
>>> perfect_optimized(496)
True
>>> perfect_optimized(12)
False
>>> perfect_optimized(1)
False
>>> perfect_optimized(0)
False
>>> perfect_optimized(-1)
False
"""
if not isinstance(number, int):
raise ValueError("number must be an integer")

if number <= 1:
return False

divisor_sum = 1 # 1 is always a proper divisor for n > 1

# Check divisors up to sqrt(number)
i = 2
while i * i <= number:
if number % i == 0:
divisor_sum += i
# Add the paired divisor if it's different from i
if i != number // i:
divisor_sum += number // i
i += 1

return divisor_sum == number


def find_perfect_numbers(limit: int) -> list[int]:
"""
Find all perfect numbers up to a given limit.

Args:
limit: The upper bound to search for perfect numbers.

Returns:
List of perfect numbers up to the limit.

Examples:
>>> find_perfect_numbers(10)
[6]
>>> find_perfect_numbers(30)
[6, 28]
>>> find_perfect_numbers(500)
[6, 28, 496]
>>> find_perfect_numbers(0)
[]
>>> find_perfect_numbers(1)
[]
"""
if not isinstance(limit, int) or limit < 0:
raise ValueError("limit must be a non-negative integer")

return [n for n in range(1, limit + 1) if perfect(n)]


def get_divisors(number: int) -> list[int]:
"""
Get all proper divisors of a number (excluding the number itself).

Args:
number: The positive integer to find divisors for.

Returns:
List of proper divisors in ascending order.

Examples:
>>> get_divisors(6)
[1, 2, 3]
>>> get_divisors(28)
[1, 2, 4, 7, 14]
>>> get_divisors(12)
[1, 2, 3, 4, 6]
>>> get_divisors(1)
[]
>>> get_divisors(7)
[1]
"""
if not isinstance(number, int) or number <= 0:
raise ValueError("number must be a positive integer")

if number == 1:
return []

return [i for i in range(1, number // 2 + 1) if number % i == 0]


if __name__ == "__main__":
from doctest import testmod

testmod()
print("Program to check whether a number is a Perfect number or not...")
try:
number = int(input("Enter a positive integer: ").strip())
except ValueError:
msg = "number must be an integer"
raise ValueError(msg)
print("Running doctests...")
testmod(verbose=True)

print("\nPerfect Number Checker")
print("=" * 40)
print("A perfect number equals the sum of its proper divisors.")
print("Examples: 6 (1+2+3), 28 (1+2+4+7+14), 496, 8128, ...")
print()

while True:
try:
user_input = input("Enter a positive integer (or 'q' to quit): ").strip()
if user_input.lower() == "q":
break

number = int(user_input)

if number <= 0:
print("Please enter a positive integer.")
continue

is_perfect = perfect(number)
divisors = get_divisors(number)
divisor_sum = sum(divisors)

print(f"\nNumber: {number}")
print(f"Proper divisors: {divisors}")
print(f"Sum of divisors: {divisor_sum}")
print(f"Is perfect: {'Yes' if is_perfect else 'No'}")

if is_perfect:
print(f"✓ {number} is a Perfect Number!")
else:
print(f"✗ {number} is not a Perfect Number.")

print("-" * 40)

print(f"{number} is {'' if perfect(number) else 'not '}a Perfect Number.")
except ValueError as e:
if "invalid literal" in str(e):
print("Please enter a valid integer.")
else:
print(f"Error: {e}")
except KeyboardInterrupt:
print("\nGoodbye!")
break