3
\$\begingroup\$

It would be helpful to know where I could improve the style of it, so it follows the style guide better, and also any ways I could compact it - especially at the end, where I have added items to a list then removed them. Is there a more elegant way to do that?

def reptillint(sentence,mini,maxi,error1):
    maxi = int(maxi)
    number = ''
    while number == '':
        number = input(sentence+' (whole number from ' + str(mini) + ' to ' + str(maxi) + ') ')
        try:
            number = int(number)
            if (number < mini) | (number > maxi):
                number = ''
                print (error1)            
        except ValueError:
            number = ''
            print ('Needs to be a number')

    return number

contestants = ''
while (contestants == ''):
    contestants = input('How many people are competing (whole number, min 2)? ')
    try:
        int(contestants)
    except ValueError:
        contestants = ''
        print ('Needs to be a number')


listOfContestantNumbers = [ i for i in range(1,int(contestants)+1)]
listOfContestantNumbersTimes = [ 'N/C' for i in range(1,int(contestants)+1)]
listOfContestantNumbersTimesSeconds = [ 'N/C' for i in range(1,int(contestants)+1)]
listOfContestantNumbersTimesSecondsstr = [ 'N/C' for i in range(1,int(contestants)+1)]


while 'N/C' in listOfContestantNumbersTimes:


    number = reptillint('What is the number of the person who just finished?',0,contestants,'Needs to be a contestant\'s number')

    print (number)

    hours = reptillint('Hours?',0,10000,'Needs to be more than 0')
    minutes = reptillint('Minutes?',0,59,'Needs to be more than 0 and less than 59')
    seconds = reptillint('Seconds?',0,59,'Needs to be more than 0 and less than 59')
    miliseconds = reptillint('Miliseconds?',0,100,'Needs to be more than 0 and less than 100')

    listOfContestantNumbersTimes[number-1] = str(hours)+':'+str(minutes)+':'+str(seconds)+'.'+str(miliseconds)
    listOfContestantNumbersTimesSeconds[number-1] = hours*3600+minutes*60+seconds+miliseconds/100
    listOfContestantNumbersTimesSecondsstr[number-1] = str(listOfContestantNumbersTimesSeconds[number-1])

    listOfContestantNumbers.insert(0, "Con Num")
    listOfContestantNumbersTimesSecondsstr.insert(0, "Time H:M:S")
    listOfContestantNumbersTimes.insert(0, "Time H:M:S")


    maxLengthTimes = max([len(i) for i in listOfContestantNumbersTimesSecondsstr])
    maxLengthC = max(len(str(contestants)), len("Con Num"))    

    print ('┌──'+'─'*(maxLengthC+4)+'─┬─'+'─'*(maxLengthTimes+5)+'─┐')
    for i in range (0, int(contestants)):
        print ('│  '+str(listOfContestantNumbers[i])+' '*(maxLengthC+4-len(str(listOfContestantNumbers[i])))+' │ '+str(listOfContestantNumbersTimes[i])+' '*((maxLengthTimes+5)-len(str(listOfContestantNumbersTimes[i])))+' │')
        print ('├──'+'─'*(maxLengthC+4)+'─┼─'+'─'*(maxLengthTimes+5)+'─┤')

    print ('│  '+str(listOfContestantNumbers[-1])+' '*(maxLengthC+4-len(str(listOfContestantNumbers[-1])))+' │ '+str(listOfContestantNumbersTimes[-1])+' '*((maxLengthTimes+5)-len(str(listOfContestantNumbersTimes[-1])))+' │')
    print ('└──'+'─'*(maxLengthC+4)+'─┴─'+'─'*(maxLengthTimes+5)+'─┘')

    listOfContestantNumbers.pop(0)
    listOfContestantNumbersTimesSecondsstr.pop(0)
    listOfContestantNumbersTimes.pop(0)

This was written for this question on Bicycles SE.

\$\endgroup\$

2 Answers 2

5
\$\begingroup\$

Here is how I would have approached it:

import datetime
from operator import itemgetter

def get_int_input(prompt, min_=0, max_=None):
    """Get a valid integer input."""
    while True:
        try:
            i = int(input(prompt))
        except ValueError:
            print("Please enter an integer.")
        else:
            if min_ is not None and i < min_:
                print("Must be at least {0}.".format(min_))
                continue
            elif max_ is not None and i > max_:
                print("Must be at most {0}.".format(max_))
                continue
            return i

def get_time():
    """"Get a time input as a datetime.time object."""
    h = get_int_input("Hours (0-23): ", max_=23)
    m = get_int_input("Minutes (0-59): ", max_=59)
    s = get_int_input("Seconds (0-59): ", max_=59)
    ms = get_int_input("Milliseconds (0-999): ", max_=999)
    return datetime.time(h, m, s, ms*1000)

def get_results(competitors):
    """Get a dict of finishing times for all competitors."""
    results = {}
    for _ in range(competitors):
        while True:
            competitor = get_int_input("Enter competitor number: ", min_=1, 
                                       max_=competitors)
            if competitor not in results:
                results[competitor] = get_time()
                break
            print("Time already entered.")
    return results

def print_results(results):
    """Display the race results in a table, fastest first."""
    line = "-" * 32
    print(line)
    print("| Competitor | Time (H:M:S)    |")
    for n, t in sorted(results.items(), key=itemgetter(1)):
        print(line)
        print("| {0:<10d} | {1!s:<15} |".format(n, t))
    print(line)

def race():
    """Handle race times for a user-specified number of competitors."""
    n = get_int_input("Enter number of competitors (2-): ", min_=2)
    results = get_results(n)
    print_results(results)

if __name__ == "__main__":
    race()

Note the following:

  1. Use of datetime.time to handle times more neatly;
  2. Split up into single-purpose functions to minimise duplication and make development and testing easier;
  3. if __name__ == "__main__": guard to allow easier import of functions elsewhere;
  4. Use of str.format to handle the text; and
  5. Adherence to the style guide, including variable and function names, docstrings and line lengths.
\$\endgroup\$
4
  • \$\begingroup\$ Few points: 1) I moved the print_results(results) line to be the line before return results of get_results. 2) I changed the print results to use these unicode box shapes, so it is all joined, and 3) removed the hours limit. Could I edit those in? \$\endgroup\$
    – Tim
    Sep 1, 2014 at 8:08
  • \$\begingroup\$ 1) That breaks encapsulation; what if you later want to do something else with results (e.g. add a GUI)? 2) Fine. 3) That limit is imposed by datetime.time (see the docs I linked in the answer), so you'd have to change to e.g. timedelta if you need to time longer than a day. \$\endgroup\$
    – jonrsharpe
    Sep 1, 2014 at 8:12
  • \$\begingroup\$ What is a way to get it to print every time I give it a new time, to make it give a live table output? \$\endgroup\$
    – Tim
    Sep 1, 2014 at 8:14
  • 1
    \$\begingroup\$ There are multiple approaches to modifying the code. I would probably alter get_results to get_result, which adds a single result to its argument and returns None (as it modifies the dictionary in-place). Then the for loop would move out to race, and also include a call to print_results. I would probably modify print_results to add an optional parameter for the number to print, allowing e.g. only the top five to be shown in the for loop then all results to be shown once all are in. \$\endgroup\$
    – jonrsharpe
    Sep 1, 2014 at 8:44
1
\$\begingroup\$

Input

It's good that you extracted the input and validation code into a function. It took me a while to figure out that reptillint() had nothing to do with reptiles, but probably meant "repeat until an integer is obtained". There are many better names possible, such as input_valid_int(). Curiously, you didn't try to reuse that function when asking for the number of contestants.

Requiring the user to input times as four separate integers for hours, minutes, seconds, and milliseconds is cumbersome. There should be a way to enter 1:23:07.342 or 1h23m7.342s or even 1 23 7.342 (for quick data entry). I propose that you accept a string and try to parse it using a regular expression, and perform validation at the same time.

Representation

Using four arrays seems excessive. Conceptually, you should only need one array to store all contestants' times.

Avoid storing the string 'N/C' to indicate the absence of a value. The idiomatic approach would be to leave those values uninitialized to None. By using 'N/C', you would be mixing strings and numbers in your data, which feels icky. Furthermore, the 'N/C' representation is a presentation-layer concept; you should be doing that translation in the output routine instead.

Main loop

There is a bug in the main loop: you allow a "contestant 0", which gets interpreted as the last contestant.

Output

The output routine deserves to be a function, pulled out of the main loop.

The huge string concatenation is hard to read. str.format() is the way to go, I think. You want a mini-function that takes a value for the left column and a value for the right column and produces a line of output.

Using a datetime.timedelta, as @jonrsharpe suggested, also simplifies the conversion of durations to a string representation.

Suggested rewrite

from datetime import timedelta
import re

HOURS_RE = r'(?P<hours>\d+)(?:\s+|\s*:\s*|\s*h\s*)'
MINS_RE  = r'(?P<minutes>[0-5]?\d)(?:\s+|\s*:\s*|\s*m\s*)'
SECS_RE  = r'(?P<seconds>[0-5]?\d(?:\.\d*)?(?:\s*s\s*)?)'
TIME_RE  = re.compile(r'\A(?:(?:%s)?%s)?%s\Z' % (HOURS_RE, MINS_RE, SECS_RE),
                      re.IGNORECASE)

def parse_timedelta(expr):
    match = TIME_RE.match(expr)
    if not match:
        raise ValueError('Invalid time: %s' % (expr))

    hours   = int(match.group('hours') or 0)
    minutes = int(match.group('minutes') or 0)
    seconds = float(match.group('seconds'))
    return timedelta(hours=hours, minutes=minutes, seconds=seconds)

# http://stackoverflow.com/a/8294654
def err(message):
    raise ValueError(message)

def ask(prompt, transform=str, validate=lambda val: True):
    while True:
        try:
            user_input = transform(input(prompt))
            validate(user_input)
        except ValueError as e:
            print('Error: ' + str(e))
        else:
            return user_input

def display(contestant_times):
    max_contestant_len = len(str(len(contestant_times) + 1))
    max_time_len = len(str(max(contestant_times, key=lambda t: len(str(t)))))
    headings = ('Con Num', 'Time H:M:S')
    field_widths = (max(len(headings[0]), max_contestant_len),
                    max(len(headings[1]), max_time_len))

    # Use format() twice: the first time to generate a format string with
    # the desired field widths
    def fields(template, *args):
        print(template.format(*field_widths).format(*args))

    fields('┌─{{0:─^{0}}}─┬─{{1:─^{1}}}─┐', '', '')
    fields('│ {{0: >{0}}} │ {{1: >{1}}} │', *headings)
    for who, time in enumerate(contestant_times):
        fields('├─{{0:─^{0}}}─┼─{{1:─^{1}}}─┤', '', '')
        fields('│ {{0: >{0}}} │ {{1: >{1}}} │',
               who + 1, 'N/C' if time is None else str(time))
    fields('└─{{0:─^{0}}}─┴─{{1:─^{1}}}─┘', '', '')


def main():
    n_contestants = ask('How many people are competing? ', int,
                        lambda n: (2 <= n) or err('minimum 2'))
    contestant_times = [None] * n_contestants

    try:
        while None in contestant_times:
            who = ask('What is the number of the person who just finished? ',
                      int,
                      lambda n: (1 <= n <= n_contestants) or err("invalid contestant's number"))
            contestant_times[who - 1] = ask("Contestant %d's time? " % (who),
                                            parse_timedelta)
            display(contestant_times)
    except EOFError:
        print()
        display(contestant_times)

if __name__ == '__main__':
    main()
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Note that timedelta doesn't have such a stable string representation - it doesn't zero-pad the hour (as it could be more than two places), so you can't left-align as you can with a time object. Both will ditch the fractional seconds where they would be .000000, so you can't right-align either. \$\endgroup\$
    – jonrsharpe
    Sep 2, 2014 at 13:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.