    # Magic, happy, automorphic, narcissistic, handsome... numbers

There are many kinds of numbers: natural numbers, integers, rational numbers, real numbers, complex numbers, magic numbers, even happy numbers. We are going to extend the functionality of our class myNumber to recognize them.

Harshad numbers are those that are divisible by the sum of their own digits, e.g., 1 2 3 4 5 6 7 8 9 10 12 18 20 21 24 27 30 36 40…

738 is a Harshad number because 7 + 3 + 8 = 18 and 738 % 18 equals cero (738 = 18 * 41).

``````def isHarshadNumber(self):
""" It checks if self is a Harshad or Niven number."""
if self.number == 0:
return False

sum = 0
for _, digit in enumerate(str(self.number)):
sum += int(digit)

return (self.number % sum == 0)
``````

# Magic numbers

A magic number is that number whose repeated sum of its digits till the sum becomes a single digit is equal to 1, e.g., 1 10 19 28 37 46 55 64 73 82 91 100 109 118 127 136 145 154…

847 is a magic number because 8 + 4 + 7 = 19, 1 + 9 = 10, 1 + 0 = 1.

``````def numDigits(self):
""" It returns the number of digits."""
return len(str(self.number))

def copy(self):
""" It returns a copy of the object."""
return myNumber(str(self.number), self.base)

def isMagicNumber(self):
""" It checks if self is a magic number."""
temporaryNumber = self.copy()
sumDigits = self.number

while temporaryNumber.numDigits() != 1:
sumDigits = 0
for i in range(len(str(temporaryNumber.number))):
sumDigits += int(str(temporaryNumber.number)[i])

temporaryNumber = myNumber.fromInteger(sumDigits)

return sumDigits == 1
``````

# Happy numbers

A happy number is one that eventually reaches 1 when replaced by the sum of the square of each digit, e.g., 1 7 10 13 19 23 28 31 32 44 49 68 70 79 82 86 91 94 97…

49 is a happy number because 42 + 92= 97, 92 + 72= 130, 12 + 32 + 02=10, 12 + 02= 1.
``````""" It checks if self is a happy number. """
st = set() # Firstly, we create a set object to save all the previously calculated sum of squares.
n = self.number
while 1:
# Secondly, we calculate the sum of the squares of each digit.
squareSum = 0
while n:
squareSum += pow(n % 10, 2)
n = int(n / 10)

if (squareSum == 1): # It is a happy number, the sum has reached one.
return True
if squareSum in st: # If the sum of the squares of each digit has previously been calculated, then it is not a happy number.
return False
n = squareSum
``````

# Palindrome numbers

A palindrome number is one that remains the same when its digits are reversed (when read backward), e.g., 1 2 3 4 5 6 7 8 9 11 22 33 44 55 66 77 88 99 101 111 121 131 141…

9889 is a palindrome number because it reads the same backward as forward.

``````def isPalindromeNumber(self):
""" It checks if self is a palindrome number."""
return str(self.number) == str(self.number)[::-1]
``````

# Automorphic numbers

An automorphic number is one _whose square ends in the same digits as the number itself., e.g., 0 1 5 6 25 76 376 625…

625 is an automorphic number because 6252 = 390625.

``````""" It checks if self is an automorphic number. """
square = math.pow(self.number, 2)
value = self.number
while value != 0:
squareRemainder, valueRemainder = square % 10, value % 10
# It checks if the last digit of value is the same digit than the last one of square.
if squareRemainder != valueRemainder:
return False
# We reduce square and value by dropping the last digit.
square, value = square // 10, value // 10
return True
``````

# Lucky numbers

Lucky numbers are those that require a number of sieves (each one of them eliminates some numbers based on their positions) to separate them from the rest.

First, it crosses out every second number and leaves only the odd numbers: 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31 … Next, the second remaining number is three, so it removes every third number: 1, 3, 7, 9, 13, 15, 19, 21, 25, 27, 31 … (it eliminates 5, 11, 17, 23). The next remaining number is 7, so it removes every 7th number (1, 3, 7, 9, 13, 15, 21, 25, 27, 31 …; it eliminates 19) and so on, e.g., 1 3 7 9 13 15 21 25 31 33 37 43 49 51 63 67 69 73 75 79 87 93 99 105…

``````def isLuckyNumber(self, position=None, nextRemove=2):
""" It checks if self is a lucky number. It is based on @sai, stackoverflow.com"""
if self.number == 0:
return False
elif self.number == 1:
return True

numbers = [i for i in range(1, self.number*2+1)]
index = 1
next_freq = numbers[index] # next_freq = 2.
while next_freq < len(numbers):
del numbers[next_freq-1::next_freq] # This is where the sieve comes in. 1st time: index = 1, next_freq=2, del numbers[1::2]. 2nd time: index = 1, next_freq=3, del numbers[2::3]
if next_freq in numbers:
index += 1
next_freq = int(numbers[index])
else: # If index = 1, next_freq = 2 (the sieve has crossed out every second number and it has left only the odd numbers), numbers = 1, 3, 5, 7, 9, 11, 13, 15, 17, ..., the following next_freq should be 3 so we should not update index.
next_freq = int(numbers[index])

if self.number not in numbers: # is our number a surviving (lucky) number?
return False

return self.number in numbers
``````

# Narcissistic numbers

A narcissistic number is a number equal to the sum of its digits raised to the power of the number of digits, e.g., 1 2 3 4 5 6 7 8 9 153 370 371 407 1634 8208 9474.

8208 is a narcissistic number because 8208 = 84 + 24 + 04 + 84= 8208.

``````def isNarcissisticNumber(self):
""" It checks if self is a narcissistic number."""
sum = 0
number = str(self.number)
for i in range(len(number)):
sum += math.pow(int(number[i]), len(number))

return sum == self.number
``````

# Triangular numbers

A triangular number is one obtained by adding all of the natural numbers up to a certain number, e.g., 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 210 231 253 276 300 325 351 378 406 435 465 496…

21 is a triangular number because 1 + 2 + 3 + 4 + 5 + 6 = 21.

``````def isTriangularNumber(self):
""" It checks if self is a triangular number."""
if self.number == 0:
return False
sum, i = 0, 0
while sum <= self.number:
sum += i
if sum == self.number:
return True
i += 1
return False
``````

# Handsome numbers

A handsome number is one in which the sum of all the left side digits is equal to the last digit, e.g., 11 22 33 44 55 66 77 88 99 101 112 123 134 145 156 167 178 189 202 213 224…

527 is a handsome number because 5 + 2 = 7.

``````def isHandsomeNumber(self):
""" It checks if self is a handsome number."""
sum = 0
number = str(self.number)
for i in range(len(number)-1):
sum += int(number[i])

return sum == int(number[len(number)-1])
``````

# Strong numbers

A strong number is one in which the sum of the factorial of all its digits is equal to the number itself, e.g., 1 2 145 40585

145 is a strong number because 145=1! + 4! + 5! = 1 + 24 + 120.

``````def isStrongNumber(self):
""" It checks if self is a strong number."""
sum = 0
number = str(self.number)
for i in range(len(number)):
sum += math.factorial(int(number[i]))

return sum == self.number
``````

# Powerful numbers

A powerful number is one in which for every prime number p dividing it, p^2 also divides it. In other words, every prime that appears in its prime factorization appears there at least twice, e.g., 1 4 8 9 16 25 27 32 36 49 64 72 81 100 108 121 125 128 144 169 196 200 216…

216 is a powerful number because 216 = 23 33.

``````def isPowerfulNumber(self):
""" It checks if self is a strong number."""
for primeDivisor in dict.keys(factorint(self.number)):
if self.number % (primeDivisor*primeDivisor) != 0:
return False
return True
``````

# Perfect numbers

A perfect number is one in which the sum of all its proper divisors is equal to the number itself, e.g., 6 28 496 8128…

A proper divisor is a divisor of a number which is not the number itself. 496 is a perfect number because 496 = 1 + 2 + 4 + 8 + 16 + 31 + 62 + 124 + 248.

``````def isPerfectNumber(self):
""" It checks if self is a perfect number."""
aliquotSum = 0
for divisor in divisors(self.number):
if divisor != self.number:
aliquotSum += divisor

return aliquotSum == self.number
``````

# Kaprekar numbers

A Kaprekar number is one that when squared, can be divided into two numbers which add up to the original number and none of them is zero, e.g., 1 9 45 55 99 297 703 999…

45 is a Kaprekar number because 452 = 2025 and 20 + 25 = 45.

``````def isKaprekar(self):
""" It checks if self is a Kaprekar number."""
if self.number == 0:
return False
elif self.number == 1:
return True
n2 = str(self.number**2)
for i in range(1, len(n2)): # We iterate over all possible ways of splitting the square number into two numbers (a, b)
a, b = int(n2[:i]), int(n2[i:])
if b != 0 and a + b == self.number:
return True

return False
``````

# Wobbly numbers

A wobbly number is one in which its digits alternate between being higher and lower than the preceding one, e.g., 73638, 64549, 51878, 47231, 42658…

42658 is a wobbly number because 4>2, 2<6, 6>5, 5<8.

``````def isWobbly(self):
""" It checks if self is a wobly number."""
if len(str(self.number)) < 2:
return False
currentDigit = int(str(self.number))
previousDigit = int(str(self.number))
if currentDigit == previousDigit:
return False

isLower = previousDigit < currentDigit
for i in range(2, len(str(self.number))):
nextDigit = int(str(self.number)[i])
if isLower and currentDigit < nextDigit or not isLower and currentDigit > nextDigit:
return False
elif currentDigit == nextDigit:
return False
isLower = currentDigit < nextDigit
currentDigit = nextDigit

return True
``````

# Python deficient and abundant numbers

A python deficient number is one in which the sum of its divisors is less than twice the number or the sum of its proper divisors is less than the number itself, e.g., 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 19, 21, 22, 23, and 24. An abundant number is a number where the sum of all its proper divisors is greater than twice the number, e.g., 12, 18, 20, 24, 30, 36, 40, 42, 48, 54, 56, 60, 66, 70, and 72.

136 is a deficient number because Factors(136) = [1, 2, 4 , 8, 17, 34, 68, 136] and 1 + 2 + 4 + 8 + 17 + 34 + 68 + 136 = 270 < 136*2 = 272. 18 is an abundant number because Factor(18) = [1, 2, 3, 6, 9, 18] and 1 + 2 + 3 + 6 + 9 + 18 = 39 > 2 * 18 = 36.

``````def isDeficientNumber(self):
""" It checks if self is a deficient number."""
aliquotSum = 0
for divisor in divisors(self.number):
aliquotSum += divisor

return aliquotSum < self.number * 2

def isAbundantNumber(self):
""" It checks if self is an abundant number."""
aliquotSum = 0
for divisor in divisors(self.number):
aliquotSum += divisor

return aliquotSum > self.number*2
``````

# Amicable numbers

Amicable numbers are two numbers related in such a way that the sum of the proper divisors of each is equal to the other number.

220, 284 are amicable number because Divisors(220)= 1, 2, 4, 5, 10, 11, 20, 22, 44, 55 and 110, 1 + 2 + 4 + 5 + 10 + 11 + 20 + 22 + 44 + 55 + 110 = 284, Divisors(284) = 1, 2, 4, 71 and 142, and 1 + 2 + 4 + 71 + 142 = 220.

``````    def sumProperDivisors(self):
""" It returns the sum of all its proper divisors."""
aliquotSum = 0
for divisor in divisors(self.number):
if divisor != self.number:
aliquotSum += divisor

return aliquotSum

def amicableNumber(self, other):
""" It checks if self and other are amicable numbers. Usage: print(myNumber("220").amicableNumber(myNumber("285")))
returns True."""
return self.sumProperDivisors() == other.number and  other.sumProperDivisors() == self.number

@staticmethod
def listLuckyNumbers(n):
""" It returns a list with all the lucky numbers in the range [1..n-1]."""
list = ""
if not isinstance(n, int) or n < 1:
return
for i in range(1, n):
numberI = myNumber.fromInteger(i)
if numberI.isLuckyNumber():
list += " " + str(numberI)
return list

@staticmethod
def listAutomorphicNumbers(n):
""" It returns a list with all the automorphic numbers in the range [0..n-1]."""
list = ""
if not isinstance(n, int) or n < 0:
return
for i in range(0, n):
numberI = myNumber.fromInteger(i)
if numberI.isAutomorphic():
list += " " + str(numberI)
return list

@staticmethod
def listTriangularNumbers(n):
""" It returns a list with all the triangular numbers in the range [0..n-1]."""
list = ""
if not isinstance(n, int) or n < 0:
return
for i in range(0, n):
numberI = myNumber.fromInteger(i)
if numberI.isTriangularNumber():
list += " " + str(numberI)
return list

@staticmethod
""" It returns a list with all the palindrome numbers in the range [1..n-1]."""
list = ""
if not isinstance(n, int) or n < 1:
return
for i in range(1, n):
numberI = myNumber.fromInteger(i)
list += " " + str(numberI)
return list

@staticmethod
def listNarcissisticNumbers(n):
""" It returns a list with all the narcissistic numbers in the range [1..n-1]."""
list = ""
if not isinstance(n, int) or n < 1:
return
for i in range(1, n):
numberI = myNumber.fromInteger(i)
if numberI.isNarcissisticNumber():
list += " " + str(numberI)
return list

@staticmethod
""" It returns a list with all the handsome numbers in the range [1..n-1]."""
list = ""
if not isinstance(n, int) or n < 1:
return
for i in range(1, n):
numberI = myNumber.fromInteger(i)
list += " " + str(numberI)
return list

@staticmethod
def listStrongNumbers(n):
""" It returns a list with all the strong numbers in the range [1..n-1]."""
list = ""
if not isinstance(n, int) or n < 1:
return
for i in range(1, n):
numberI = myNumber.fromInteger(i)
if numberI.isStrongNumber():
list += " " + str(numberI)
return list

@staticmethod
def listPerfectNumbers(n):
""" It returns a list with all the perfect numbers in the range [1..n-1]."""
list = ""
if not isinstance(n, int) or n < 1:
return
for i in range(1, n):
numberI = myNumber.fromInteger(i)
if numberI.isPerfectNumber():
list += " " + str(numberI)
return list

@staticmethod
def listPowerfulNumbers(n):
""" It returns a list with all the powerful numbers in the range [1..n-1]."""
list = ""
if not isinstance(n, int) or n < 1:
return
for i in range(1, n):
numberI = myNumber.fromInteger(i)
if numberI.isPowerfulNumber():
list += " " + str(numberI)
return list

if __name__ == '__main__':
if myNumber("28").isPerfectNumber():
print(str(myNumber("28")) + " is a perfect number.")
else:
print(str(myNumber("28")) + " is not a perfect number.")
# Result: 28 is a perfect number.
print("Perfect numbers")
print(myNumber.listPerfectNumbers(10000))
# 6 28 496 8128

if myNumber("49").isPowerfulNumber():
print(str(myNumber("49")) + " is a powerful number.")
else:
print(str(myNumber("49")) + " is not a powerful number.")
# Result: 49 is a powerful number.
print("Powerful numbers")
print(myNumber.listPowerfulNumbers(1000))
# Result: 1 4 8 9 16 25 27 32 36 49 64 72 81 100 108 121 125 128 144 169...

if myNumber("145").isStrongNumber():
print(str(myNumber("145")) + " is a strong number.")
else:
print(str(myNumber("145")) + " is not a strong number.")
# Result: 145 is a strong number.
print("Strong numbers")
print(myNumber.listStrongNumbers(100000))
# Result: 1 2 145 40585

print(str(myNumber("347")) + " is a handsome number.")
else:
print(str(myNumber("347")) + " is not a handsome number.")
# Result: 347 is a handsome number.
print("Handsome numbers")
# 11 22 33 44 55 66 77 88 99 101 112 123 134 145 156 167 178 189 202 213 224 235 246...

if myNumber("371").isNarcissisticNumber():
print(str(myNumber("371")) + " is a narcissistic number.")
else:
print(str(myNumber("371")) + " is not a narcissistic number.")
# Result: 371 is a narcissistic number.
print("Narcissistic numbers")
print(myNumber.listNarcissisticNumbers(10000))
# Result: 1 2 3 4 5 6 7 8 9 153 370 371 407 1634 8208 9474

print(str(myNumber("14341")) + " is a palindrome number.")
else:
print(str(myNumber("14341")) + " is not a palindrome number.")
# Result: 14341 is a palindrome number.
print("Palindrome numbers")
# Result: 1 2 3 ... 11 22 33 ... 101 111 121 131 ... 191 202 212 ... 1001 1111 1221...

if myNumber("325").isTriangularNumber():
print(str(myNumber("325")) + " is a triangular number.")
else:
print(str(myNumber("325")) + " is not a triangular number.")
# Result: 325 is a triangular number.
print("Triangular numbers")
print(myNumber.listTriangularNumbers(500))
# Result: 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 210 231 253...

if myNumber("76").isAutomorphic():
print(str(myNumber("76")) + " is an automorphic number.")
else:
print(str(myNumber("76")) + " is not an automorphic number.")
# Result: 76 is an automorphic number.
print(myNumber.listAutomorphicNumbers(1000))
# Result:  0 1 5 6 25 76 376 625.
``````

# Refactoring Our Code

Why do we have so much duplicated code? As a general rule, you remove duplicate code by factoring the common code into a function, method or class and parameterizing the parts that vary.

``````    @staticmethod
def listNumbers(n, mymethod):
""" It returns a list with all the numbers in the range [0..n-1] that satisfy "mymethod".
Usage: myNumber.listNumbers(1000, "isAutomorphic"). It is understood that the method "isAutomorphic" docstring is Automorphic Numbers."""
list = ""
template = str(getattr(myNumber, mymethod).__doc__) + "[red][ %s ][/red]\\n" # getattr(myNumber, mymethod).__doc__ returns the method's docstring.
if not isinstance(n, int) or n < 0:
return
for i in range(0, n):
numberI = myNumber.fromInteger(i)
if getattr(numberI, mymethod)():
# This is the way to dynamically call a method in Python.
# Methods are just attributes, so we use getattr() to retrieve them dynamically.
list += " " + str(numberI)
return template % "".join(list)

@staticmethod
def isNumber(mymethod):
""" It generates a random number and checks if it satisfies "mymethod". Usage:  myNumber.isNumber("isMagicNumber"). It is understood that the method "isMagicNumber" docstring is Magic Numbers."""
list = ""

myRandomNumber = myNumber.random() # It generates a random number.
nameNumber = str(getattr(myNumber("0"), mymethod).__doc__) # getattr(myNumber, mymethod).__doc__ returns the method's docstring.
if getattr(myRandomNumber, mymethod)():
# This is the way to dynamically call a method in Python. Methods are just attributes, so we use getattr() to retrieve them dynamically.
# This line checks if myRandomNumber satisfies "mymethod".
print(str(myRandomNumber) + " is a" +
str(getattr(myNumber("0"), mymethod).__doc__))
else:
print(str(myRandomNumber) + " is not a" +
str(getattr(myNumber("0"), mymethod).__doc__))

def isNumber2(self, mymethod):
""" It checks if self satisfies "mymethod". Usage: myNumber("738").isNumber2("isHarshadNumber")."""
list = ""

nameNumber = str(getattr(self, mymethod).__doc__) # getattr(myNumber, mymethod).__doc__ returns the method's docstring.
if getattr(self, mymethod)():
# This is the way to _dynamically call a method in Python. Methods are just attributes, so we use getattr() to retrieve them dynamically.
# This line checks if self satisfies "mymethod".
print(str(self) + " is a" + nameNumber)

else:
print(str(self) + " is not a" + nameNumber)

if __name__ == '__main__':
print(myNumber.listNumbers(1000, "isAutomorphic")) # Automorphic Numbers. [ 0 1 5 6 25 76 376 625 ]. We have conveniently change the method's docstring to server our requirements.
print(myNumber.listNumbers(1000, "isMagicNumber"))  # Magic Numbers. [ 1 10 19 28 37 46 55 64 73 82 91 100 109 118 127 136...]
myNumber.isNumber("isMagicNumber") # 878 is not a Magic Number.
print(myNumber.listNumbers(1000, "isHappyNumber")) # Happy Numbers. [ 1 7 10 13 19 23 28 31 32 44 49 68 70 79 82 86 91 94 97 100...] 