[Tutor] How to refactor a simple, straightforward script into a "proper" program?

boB Stepp robertvstepp at gmail.com
Wed Jan 1 21:38:20 EST 2020


On Wed, Jan 1, 2020 at 12:30 PM boB Stepp <robertvstepp at gmail.com> wrote:
> <version1.py => A simple throwaway script>
> ============================================================================
> #!/usr/bin/env python3
> """Calculate the number of pages per day needed to read a book by a
> given date."""
>
> from datetime import date
>
> COUNT_TODAY = 1
>
> num_pages_in_book = int(input("How many pages are there in your book?  "))
> num_pages_read = int(input("How many pages have you read?  "))
> today_date = date.today()
> goal_date = date.fromisoformat(
>     input("What is your date to finish the book (yyyy-mm-dd)?  ")
> )
>
> days_to_goal = (goal_date - today_date).days
> pages_per_day = (num_pages_in_book - num_pages_read) / (days_to_goal +
> COUNT_TODAY)
> print(
>     "\nYou must read",
>     pages_per_day,
>     "pages each day (starting today) to reach your goal.",
> )
> ============================================================================
>
> As this script is I believe it to be easy to read and understand by an
> outside reader.  For my original purposes it quickly gave me what I
> wanted out of it.  But if I wanted to "give it away" to the general
> public there are some things that bother me:
>
> 1)  There is no error checking.  If someone deliberately or mistakenly
> enters incorrect input the program will terminate with a ValueError
> exception.
> 2)  There are no checks for logic errors.  For instance, a person
> could enter a goal date that is prior to today or enter a date in the
> future that falls well beyond any human's possible lifespan (with
> current technology).
> 3)  There is no formatting of output.  It is possible to generate
> float output with many decimal places, something most users would not
> want to see.  And does it make any sense to have non-integral numbers
> of pages anyway?
> 4)  There are duplicated patterns of input in the code as is.  Surely
> that duplication could be removed?
> 5)  A minor, but bothersome quibble:  If the reader need only read one
> page per day, the program would display an annoying "... 1.0 pages
> ...", which is grammatically incorrect.
> 6)  There are no tests, which makes it more difficult to grow/maintain
> the program in the future.
> 7)  The user must restart the program each time he/she wishes to try
> out different goal dates.
> 8)  [Optional] There are no type annotations.

OK, I have made some progress, but this has been for me a more
difficult exercise than I thought it would be.  I feel that in my
efforts to be DRY that I am coding as if I were wet behind my ears!
Anyway, here is what I currently have:

<version2.py => Not all goals accomplished -- yet.>
============================================================================
#!/usr/bin/env python3
"""Calculate the number of pages per day needed to read a book by a
given date."""

from datetime import date


def get_input(str_converter, msg):
    """Prompt user with a message and return user input with the
correct data type."""
    while True:
        try:
            return str_converter(input(msg))
        except ValueError:
            if str_converter == int:
                print("\nPlease enter a positive integer!")
            elif str_converter == date.fromisoformat:
                print("\nPlease enter a valid date in the following
format yyyy-mm-dd!")


def get_input_params():
    """Collect all needed input parameters."""
    str_converters = [int, int, date.fromisoformat]
    input_msgs = [
        "How many pages are there in your book?  ",
        "How many pages have you read?  ",
        "What is your date to finish the book (yyyy-mm-dd)?  ",
    ]
    num_pages_in_book, num_pages_read, goal_date = map(
        get_input, str_converters, input_msgs
    )
    days_to_goal = (goal_date - date.today()).days
    return num_pages_in_book, num_pages_read, days_to_goal


def calc_pages_per_day(num_pages_in_book, num_pages_read, days_to_goal):
    """Return number of pages to read each day to attain goal."""
    COUNT_TODAY = 1
    pages_per_day = (num_pages_in_book - num_pages_read) /
(days_to_goal + COUNT_TODAY)
    return pages_per_day


def main():
    """Run program and display results."""
    input_params = get_input_params()
    print(
        "\nYou must read",
        calc_pages_per_day(*input_params),
        "pages each day (starting today) to reach your goal.",
    )


if __name__ == "__main__":
    main()
============================================================================
I think I have removed all "perceived" duplication, but I had to
struggle to get to this point.  I have never used the map() function
before, but it was the only way I was able to avoid writing multiple
calls to get_input() as separate lines.

ValueErrors are now handled, but I don't have checks in place yet for
input that does not raise an unhandled exception, but would cause
erroneous results, such as negative numbers of days, in the past
dates, etc.

I *think* the code still reads reasonably well, but is not as
straightforward to understand as the original simple script.

I have not written tests yet (Bad boB!).  Nor have I addressed better
display formatting.

I still think that you guys would do something remarkably easier, so I
am awaiting your comments with bated breath.

HAPPY NEW YEAR TO YOU AND YOURS!!!

-- 
boB


More information about the Tutor mailing list