patternpythonMinor
Kaprekar's constant
Viewed 0 times
kaprekarconstantstackoverflow
Problem
I'm new to Python and as a first exercise, I've written a function (module?) to calculate Kaprekar's constant using Python 3.
I'd like some feedback on whether this is Pythonic enough, and if I can improve it.
Update
Thanks for the feedback, everyone!
I'd like some feedback on whether this is Pythonic enough, and if I can improve it.
import collections
def kaprekar(value):
print("Starting value: %d" % value)
# Check our range
if value 9998:
print("Input value must be between 1 and 9998, inclusive.")
return
numstr = str(value)
# Pad with leading 0s if necessary
numstr = '0' * (4 - len(numstr)) + numstr
# Make sure there are at least two different digits
if len(collections.Counter(numstr)) == 1:
print("Input value must consist of at least two different digits.")
return
# If we've gotten this far it means the input value is valid
# Start iterating until we reach our magic value of 6174
n = 0
while (value != 6174):
n += 1
numstr = str(value)
# Pad with leading 0s if necessary
numstr = '0' * (4 - len(numstr)) + numstr
# Get ascending and descending integer values
asc = int(''.join(sorted(numstr)))
dec = int(''.join(sorted(numstr)[::-1]))
# Calculate our new value
value = dec - asc
print("Iteration %d: %d" % (n, value))
# We should always reach the constant within 7 iterations
if n == 8:
print("Something went wrong...")
return -1
print("Reached 6174 after %d iterations." % n)
return nUpdate
Thanks for the feedback, everyone!
Solution
Validation
Algorithm
Suggested implementation
Further enhancement
This version separates the iteration logic from the output routines by using a generator. It also makes no presuppositions about The Answer.
- The range check
if value 9998could be more Pythonically expressed asif not 1
- To ensure that there are at least two different digits, you can just use a set
.
- An easier way to zero-pad a number to four places is '{:04d}'.format(value)
. That line of code is written twice; it may be worthwhile to extract it into a function.
- Returning None
and-1to indicate validation errors and excessive iterations is unusual for Python. When you encounter an error, raise an exception instead.
Algorithm
- The implementation presupposes that you know the answer (6174, within 7 iterations). That's a bit dissatisfying at an intellectual level; it feels like a unit test. As an alternative to checking whether the known goal has been reached, you could check whether the sequence has reached a fixed point.
- You hard-code a lot of special numbers: 4, 9998, 6174, 8. It would be nice to reduce the usage of such constants, and where they are necessary, clarify their purpose. The Wikipedia page mentions that there is a 3-digit Kaprekar Process; it would be nice to have code that is easily adaptable to that related problem.
- The loop has two purposes: check if the goal has been reached, and count the number of iterations. You have chosen to make the former into the loop termination condition. (It's customary to omit the parentheses there, by the way.) I think that converting it into a counting loop works better, as it brings unity to the thee lines n = 0
,n += 1, andif n == 8: ….
- By postponing the conversion to int
when definingasc, you can simplify the derivation ofdec` a bit.
Suggested implementation
def kaprekar(value):
def digit_str(n, places):
return ('{:0%dd}' % places).format(n)
#PLACES, GOAL, ITERATION_LIMIT = 3, 495, 7
PLACES, GOAL, ITERATION_LIMIT = 4, 6174, 8
digits = digit_str(value, PLACES)
if value <= 0:
raise ValueError("Input value must be positive.")
if len(digits) != PLACES:
raise ValueError("Input value must be %d digits long." % PLACES)
if len(set(digits)) < 2:
raise ValueError("Input value must consist of at least two different digits.")
for iterations in range(ITERATION_LIMIT):
print("Iteration %d: %s" % (iterations, digits))
if value == GOAL:
print("Reached %d after %d iterations." % (GOAL, iterations))
return iterations
asc = ''.join(sorted(digits))
dsc = asc[::-1]
value = int(dsc) - int(asc)
digits = digit_str(value, PLACES)
raise StopIteration("Something went wrong...")Further enhancement
This version separates the iteration logic from the output routines by using a generator. It also makes no presuppositions about The Answer.
import sys
def kaprekar(digits):
if int(digits) <= 0:
raise ValueError("Input value must be positive.")
if len(set(digits)) < 2:
raise ValueError("Input value must consist of at least two different digits.")
places = len(digits)
prev_digits = None
while digits != prev_digits:
yield digits
prev_digits = digits
asc = ''.join(sorted(digits))
dsc = asc[::-1]
value = int(dsc) - int(asc)
digits = ('{:0%dd}' % places).format(value)
def main(_, numstr):
try:
for iterations, digits in enumerate(kaprekar(numstr)):
print("Iteration {i}: {d}".format(i=iterations, d=digits))
print("Reached {d} after {i} iterations".format(i=iterations, d=digits))
except ValueError as e:
print(e, file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main(*sys.argv)Code Snippets
def kaprekar(value):
def digit_str(n, places):
return ('{:0%dd}' % places).format(n)
#PLACES, GOAL, ITERATION_LIMIT = 3, 495, 7
PLACES, GOAL, ITERATION_LIMIT = 4, 6174, 8
digits = digit_str(value, PLACES)
if value <= 0:
raise ValueError("Input value must be positive.")
if len(digits) != PLACES:
raise ValueError("Input value must be %d digits long." % PLACES)
if len(set(digits)) < 2:
raise ValueError("Input value must consist of at least two different digits.")
for iterations in range(ITERATION_LIMIT):
print("Iteration %d: %s" % (iterations, digits))
if value == GOAL:
print("Reached %d after %d iterations." % (GOAL, iterations))
return iterations
asc = ''.join(sorted(digits))
dsc = asc[::-1]
value = int(dsc) - int(asc)
digits = digit_str(value, PLACES)
raise StopIteration("Something went wrong...")import sys
def kaprekar(digits):
if int(digits) <= 0:
raise ValueError("Input value must be positive.")
if len(set(digits)) < 2:
raise ValueError("Input value must consist of at least two different digits.")
places = len(digits)
prev_digits = None
while digits != prev_digits:
yield digits
prev_digits = digits
asc = ''.join(sorted(digits))
dsc = asc[::-1]
value = int(dsc) - int(asc)
digits = ('{:0%dd}' % places).format(value)
def main(_, numstr):
try:
for iterations, digits in enumerate(kaprekar(numstr)):
print("Iteration {i}: {d}".format(i=iterations, d=digits))
print("Reached {d} after {i} iterations".format(i=iterations, d=digits))
except ValueError as e:
print(e, file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main(*sys.argv)Context
StackExchange Code Review Q#70369, answer score: 8
Revisions (0)
No revisions yet.