HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Kaprekar's constant

Submitted by: @import:stackexchange-codereview··
0
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.

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 n


Update

Thanks for the feedback, everyone!

Solution

Validation

  • The range check if value 9998 could be more Pythonically expressed as if 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 -1 to 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, and if n == 8: ….



  • By postponing the conversion to int when defining asc, you can simplify the derivation of dec` 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.