3
\$\begingroup\$

I'm new to this, but after learning some of this in class I decided to try my hand at creating a program to get the info from a Zybooks signature which stores the run data for your project. it takes a string in the format (MM/DD..signature..MM/DD) (It does not like when the signature does not follow this syntax)

Signature ex:

11/8.. U - - - - - - - - |2 |2 |2 |6 ..11/8

10/28.. W - - - - - |1 |10 ..10/28

11/8.. U - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |4 |4 |4 |7 |0 |0 |0 - - - - - |4 |4 |4 |7 |4 |7 |5 |8 |9 |9 |9 |9 |9 |9 |9 |12 |12 |12 |14 - - - - - - - |14 |14 |14 - - - - - |16 - - - - - - - - - - - - - |16 |13 |16 |16 |16 |16 |13 - - |17 - - |17 - - |19 |19 - - - |19 |19 |17 |17 |19 - |19 |19 |19 |17 |19 - |19 |17 |17 |19 |19 |19 |17 |19 |18 - - - |18 - - - |20 |20 - - ..11/8

It gets the data from the signature and then gives you to option to print the stats or save the signature. Well, here's the code.

from datetime import datetime
import os


months_of_year = {"1": "January", "2": "February",
                  "3": "March", "4": "April",
                  "5": "May", "6": "June",
                  "7": "July", "8": "August",
                  "9": "September", "10": "October",
                  "11": "November", "12": "December"}

days_of_week = {"U": "Sunday", "M": "Monday",
                "T": "Tuesday", "W": "Wednesday",
                "R": "Thursday", "F": "Friday",
                "S": "Saturday"}


def check_save_file():
    """
    checks if save.txt exists, if not creates it
    """
    if not os.path.isfile('save.txt'):
        file = open('save.txt', 'w')
        file.close()


def save_to_file(inp):
    with open('save.txt', 'a') as file:
        file.write("X"*20)  # Header
        file.write("\n")
        cur_date = datetime.now()  # Date info for save (printed onto same line?)
        file.write(str(cur_date.month))
        file.write("/")
        file.write(str(cur_date.day))
        file.write("/")
        file.write(str(cur_date.year))
        file.write("\n")
        file.write(inp)  # Actual signature data (should be 2 indices from header ex: 2 then 4)
        file.write("\n")
        file.write("Y"*20)
        file.write("\n")  # Footer


def read_from_file():
    """
    :return:
    returns tuple where (0 = list, lines)(1 = int, save_count)(2= dic, saves)(3 = list, date)
    """
    save_count = 0
    saves = {}
    date = []
    with open('save.txt', 'r') as file:
        lines = file.readlines()  # puts save into list w/ lines

    if len(lines) == 0:  # If there are no lines, we have no save data
        return "No save data"
    for index, line in enumerate(lines):  # Want to get index and line for every line in lines
        if line[0] == "X":  # if the 0th index is 'X' it is a header
            save_count += 1
            saves[save_count] = index  # Puts save_count as the key and the index save_count points to as a value
            index += 1
            date.append(lines[index])
    return lines, save_count, saves, date


def read_save(save_num, lines, saves):
    """
    :param save_num: int
    :param lines: list
    :param saves: dic
    return tuple where 0 = date and 1 = stored signature
    :return:
    """
    return lines[saves[save_num] + 1], lines[saves[save_num] + 2]


def pull_header(usr_str):
    """
    Pulls the header from parameter usr_str, and returns
    a tuple with 0: the header, and 1:The length of the header
    """
    c = 0
    header = ""
    while usr_str[c] != usr_str[c + 1] or usr_str[c] != ".":  # Goes until ..
        header += usr_str[c]
        c += 1
    header += ".."
    return header, len(header)  # returns the header abd the length of the header


def pull_footer(usr_str):
    """
    Pulls the footer from the parameter usr_str, reverses it for input into get_date, and
    returns a tuple with 0: The reversed footer, and 1:The negated length of it
    """
    c = -1
    footer = ""
    while usr_str[c] != usr_str[c - 1] or usr_str[c] != ".":  # Goes backwards until ..
        footer += usr_str[c]
        c -= 1
    footer = footer[::-1]  # reverses the footer for use in get_date
    footer += ".."

    return footer, -len(footer)  # returns the header and the |distance form the end (DEPRECIATED)|


def get_date(header):
    """
    This function returns a date given a string with the format "MM/DD.."
    """
    c = 0
    month = ""
    day = ""
    suffix = ""
    while header[c] != "/":  # every thing before the /
        month += header[c]
        c += 1
    c += 1
    while header[c] != ".":  # Everything until the .
        day += header[c]
        c += 1

    if int(day[-1]) == 1 and int(day) != 11:  # suffixes based on the number
        suffix = "st"
    elif int(day[-1]) == 2 and int(day) != 12:
        suffix = "nd"
    elif int(day[-1]) == 3 and int(day) != 13:
        suffix = "rd"
    elif (int(day[-1]) == 0) or (int(day[-1]) > 3) or (11 <= int(day) <= 13):
        suffix = "th"

    month = months_of_year[month]

    date = "%s the %s%s" % (month, day, suffix)
    return date


def date_div(header, footer):
    """
    This functions calculates how many days the assignment took, given the header and the footer of the signature
    """
    d_s = ""
    m_s = ""
    start = header
    end = footer
    start = start[:-2]  # removes the suffix ".."
    end = end[:-2]
    start = start.split("/")  # splits the date into a list at the "/" M= 0 ,D = 1
    end = end.split("/")
    month_div = int(end[0]) - int(start[0])  # calculate the start and end differences
    day_div = int(end[1]) - int(start[1])
    if day_div > 1:
        d_s = "s"
    if month_div > 1:
        m_s = "s"
    if month_div == 0:
        if day_div == 0:
            return "You completed it on the same day!"
        elif day_div != 0:
            return "You completed it in %d day%s" % (day_div, d_s)
    elif month_div != 0:
        if day_div == 0:
            return "You completed it in %d month%s" % (month_div, m_s)
        elif day_div != 0:
            return "You completed it in %d month%s and %d day%s" % (month_div, m_s, day_div, d_s)


def translate_sig(usr_str, index):
    """
    This functions returns a string, and an integer representing the type of run, when given
    a string(usr_str) and an integer index
    """
    if usr_str[index].isalpha():  # If the character at the index is Alphabetical, stands for day of the week
        return days_of_week[usr_str[index]], 0
    elif usr_str[index] == "-":  # Stands for Ran in develop mode
        return 1, 1
    elif usr_str[index] == "|":  # This followed by an number represents ran in submit mode, followed by the score
        score = ""
        index += 1
        while usr_str[index] != " ":  # Since each entry is separated by a space, Just read until whitespace
            score += usr_str[index]
            index += 1
        return score, 2
    else:
        pass


def print_stats(dev_count, sub_count, date_dif, average, hi_score):
    """
    This function prints the statistics of your Lab, given dev_count, sub_count,
    date_dif, average, and hi_score
    """
    format_str = "{text:40}|{num:4}"  # Using fields I just learned
    print("Stats")
    print("-" * 20)
    print(format_str.format(text="Number of times ran in Dev Mode:", num=dev_count))
    print()
    print(format_str.format(text="Number of times ran in Submit Mode:", num=sub_count))
    print()
    print(format_str.format(text="Total runs:", num=dev_count + sub_count))
    print()
    print(format_str.format(text="Average score:", num=average))
    print()
    print(format_str.format(text="Highest score:", num=hi_score))
    print()
    print(date_dif)


def calc_avg_score(score_lst):
    """
    Calculates the average score, given an list of scores
    """
    length = len(score_lst)
    sc_add = 0
    for score in score_lst:
        sc_add += int(score)
    average = sc_add / length
    return round(average, 2)  # returns the average rounded by two places


def save_menu(sig):
    """

    :param sig:
        The signature (str)
    :return:
        returns a signature or 1, representing no need to cont.
    """
    op = ""
    while op != 'q':
        print("Enter 1 for save, Enter 2 for load,  q for Quit.")
        op = input()
        print()
        if op == '1':
            print("Starting Save")
            save_to_file(sig)
            print("Finished")
            print()
            return 1
        elif op == '2':
            lines, save_count, save, save_date = read_from_file()  # get variables from the returned tuple
            print("Current Saves")
            for key in save.keys():  # for every save in the save.txt prints attached # and date
                print("Save#: {}, Save Date: {}".format(key, save_date[key - 1]))
                print()
            print("Enter save # to load save")
            save_num = int(input())
            print()
            date, sig = read_save(save_num, lines, save)
            print("-"*30)
            print("Date = {}".format(date))
            print("Signature = {}".format(sig))
            print()
            print("Do want to translate this signature? (y/n)")
            trans_save = None
            cont = None
            while trans_save is None:  # continue until input is y or n
                trans_save = input()
                if trans_save.lower() == 'y':
                    cont = True
                elif trans_save.lower() == 'n':
                    cont = False
                else:
                    print("Invalid Operation")
            if cont:  # returns the signature minus newline
                return sig[:-1]
            else:
                return 1
        elif op == 'q':
            pass
        else:
            print("Invalid Operation")


def main():
    """
    Main block of the problem, 
    """
    check_save_file()
    fin = None
    done = None
    nvalid = True
    print("Copy and paste the signature below (MM/DD..SIG..MM/DD)")
    sig = input().strip()  # Get the signature and strip any trailing whitespace
    while nvalid:  # Try to see if sig is valid by checking if it has a proper header (Assumes sig is invalid)
        try:
            pull_header(sig)
        except:  # Should catch any errors
            print("Invalid Signature")
            sig = input().strip()
        else:
            nvalid = False
    while not fin:
        if not done:
            header = pull_header(sig)  # Tuple (Header , distance fromm start)
            footer = pull_footer(sig)  # Tuple (Footer , distance from end)
            print("Date Started: %s." % (get_date(header[0])))
            print()
            index = header[1]  # starting point is taken after the header finishes
            c = 1
            sub_count = 0
            dev_count = 0
            score_lst = []
            while sig[index] != ".":  # Until the program reaches "." indicating the beginning of the footer, continue translating
                op = translate_sig(sig, index)
                if op is not None:  # This is none if the index is whitespace
                    if op[-1] == 0:  # For new date
                        print("|%s| Day - %s" % (c, op[0]))

                        print()
                    elif op[-1] == 1:  # Develop Mode
                        print("%s |Ran in develop mode,  no score" % c)
                        dev_count += 1
                        c += 1
                        print()
                    elif op[-1] == 2:  # Submit Mode
                        print("%s |Ran in submit mode, score = %s" % (c, op[0]))
                        sub_count += 1
                        c += 1
                        score_lst.append(int(op[0]))  # adds score into score_lst
                        print()
                index += 1
            print("Date of last run: %s." % (get_date(footer[0])))
            done = True
            print()

        stat = ""
        get_stat = None
        get_saves = None
        cont = None  # Should only be true if another translation is called
        while not fin and not cont:
            print("Do you want to see further statistics(1) or save/load(2) (1, 2, q to quit)")
            while stat != 'q':  # If 1 is entered show stats if 2 is entered call save_menu() if q is entered exit
                stat = input()
                if stat.lower() == '1':
                    get_stat = True
                    stat = 'q'
                elif stat.lower() == '2':
                    get_saves = True
                    stat = 'q'
                elif stat.lower() == 'q':
                    fin = True
                else:
                    print("Not a valid option")  # print this and return to top of loop if not == y or n
                    print("Enter a new Option")
                    print()
            if get_stat:
                print()
                average = calc_avg_score(score_lst)
                time_taken = date_div(header[0], footer[0])
                max_score = max(score_lst)
                print_stats(dev_count=dev_count, sub_count=sub_count, date_dif=time_taken, average=average,
                            hi_score=max_score)  # prints stats
                get_stat = False

            if get_saves:
                print()
                do_op = save_menu(sig)
                if do_op != 1:  # if the opcode is anything but 1 treat is as a signature and restart the program
                    sig = do_op
                    done = False
                    fin = None
                    cont = True
                get_saves = False
            stat = ""


if __name__ == "__main__":
    main()

Now, since I'm new to this I'm pretty sure that this isn't perfect, so I'd like to know how I would be able to improve it? (It's very long as well so would there be any way for me to shorten it?) This is my first question here so I'm also pretty sure I've broken some rule or convention to posting on stack Exchange (could you let me know if I have?)so sorry about that. Thank You!

\$\endgroup\$
10
  • 1
    \$\begingroup\$ Since the signatures are always the same layout you can use str.split() to split strings. Eg. [header, signature, footer] = usr_str.split('..') \$\endgroup\$
    – Swedgin
    Commented Dec 3, 2020 at 12:34
  • 1
    \$\begingroup\$ Take a look at docs.python.org/3/library/datetime.html#module-datetime and docs.python.org/3/library/… for working with dates and time \$\endgroup\$
    – Swedgin
    Commented Dec 3, 2020 at 12:40
  • 1
    \$\begingroup\$ And again, for analyzing the sig, I'd use arr = sig.split(' '), arr[0] has the first letter and then you can loop through the rest of the array and check for lenght or first element. \$\endgroup\$
    – Swedgin
    Commented Dec 3, 2020 at 12:44
  • \$\begingroup\$ In read_from_file: index += 1; date.append(lines[index]) --> date.append(lines[index+1]). (Is this even what you want to do?) Do not change your iterator variables. \$\endgroup\$
    – Swedgin
    Commented Dec 3, 2020 at 13:10
  • 2
    \$\begingroup\$ @Graipher Oh I see, did not know that. Maybe I'll write something up tonight, \$\endgroup\$
    – Swedgin
    Commented Dec 3, 2020 at 15:23

1 Answer 1

1
\$\begingroup\$

For starters, I want to say that you've got the gist of what to do but don't know the Python language that good which results in this lengthy code.

I've rewrote the first part of your program, the part that analyzes the signature. Later on, I'll take a look at the second part.

def main():
    """
    Main block of the problem,
    """
    check_save_file()  ## --> not needed, do this when trying to read from a file
    fin = None
    done = None

    #################################
    # BLOCK INPUT, I'd move this to seperate function or use regex
    #################################
    nvalid = True
    print("Copy and paste the signature below (MM/DD..SIG..MM/DD)")
    sig = input().strip()  # Get the signature and strip any trailing whitespace
    while nvalid:  # Try to see if sig is valid by checking if it has a proper header (Assumes sig is invalid)
        try:
            pull_header(sig)
        except:  # Should catch any errors
            print("Invalid Signature")
            sig = input().strip()
        else:
            nvalid = False

    ###############################

    while not fin:
        ################################################################
        # BLOCK ANALYZE
        ################################################################
        if not done:

            ## I'd use datetime functionality

            header = pull_header(sig)  # Tuple (Header , distance fromm start)
            footer = pull_footer(sig)  # Tuple (Footer , distance from end)
            print("Date Started: %s." % (get_date(header[0])))
            print()

            index = header[1]  # starting point is taken after the header finishes
            c = 1
            sub_count = 0
            dev_count = 0
            score_lst = []
            while sig[index] != ".":  # Until the program reaches "." indicating the beginning of the footer, continue translating
                op = translate_sig(sig, index)   ## I like the use of a tuple to return the state and score
                if op is not None:  # This is none if the index is whitespace

                    ## But why op[-1] instead of op[1]? You know it's a tuple with 2 elements.

                    if op[-1] == 0:  # For new date
                        print("|%s| Day - %s" % (c, op[0]))

                        print()
                    elif op[-1] == 1:  # Develop Mode
                        print("%s |Ran in develop mode,  no score" % c)
                        dev_count += 1
                        c += 1
                        print()
                    elif op[-1] == 2:  # Submit Mode
                        print("%s |Ran in submit mode, score = %s" % (c, op[0]))
                        sub_count += 1
                        c += 1
                        score_lst.append(int(op[0]))  # adds score into score_lst
                        print()
                index += 1
            print("Date of last run: %s." % (get_date(footer[0])))
            done = True
            print()
        ################################################################

This is how I'd do it

import os
import re
from datetime import datetime

DAYS_OF_WEEK = {
    "U": "Sunday",
    "M": "Monday",
    "T": "Tuesday",
    "W": "Wednesday",
    "R": "Thursday",
    "F": "Friday",
    "S": "Saturday",
}


def date_to_string(date: datetime):
    # datetime object to string
    suffix = "th"  ## by declaring this as 'th' you don't need to check for 11, 12 and 13
    if date.day == 1:
        suffix = "st"
    elif date.day == 2:
        suffix = "nd"
    elif date.day == 3:
        suffix = "rd"

    return "{0:%B} the {0:%d}{1}".format(date, suffix)


def main():

    ## BLOCK INPUT

    # regex for matching signatures
    sig_regex = re.compile("(\d{0,2}\/\d{0,2})\.{2}.+\.{2}(\d{0,2}\/\d{0,2})")

    print("Copy and paste the signature below (MM/DD..SIG..MM/DD)")

    # while no match, keep asking for input
    while not (sig_regex.match(sig := input().strip())):
        print("Invalid Signature")

    # split input into header, body and footer
    [header, body, footer] = sig.split("..")
    body = body.strip()

    print(f"Got valid signature:\nhead:\t{header}\nbody:\t{body}\nfooter:\t{footer}")

    ## BLOCK ANALYZE

    # get the start date in format MM/dd
    start_date = datetime.strptime(header, "%m/%d")
    print(f"Date Started: {date_to_string(start_date)}\n")

    score_list = []

    # split the body into seperate elements
    body_arr = body.split(" ")

    # check the first element
    if body_arr[0] in DAYS_OF_WEEK.keys():

        print(f"|1| Day - {DAYS_OF_WEEK[body_arr[0]]}")

        # iterate over the rest of the elements
        for index, run in enumerate(body_arr[1:]):
            if run[0] == "-":
                print(f"{index+1} |Ran in develop mode, no score")
            elif run[0] == "|":
                score_list.append(int(run[1:]))
                print(f"{index+1} |Ran in submit mode, score = {score_list[-1]}")

    # get the start date in format MM/dd
    end_date = datetime.strptime(footer, "%m/%d")
    print(f"\nDate Ended: {date_to_string(end_date)}")

    print(f"\nScore list: {score_list}")


if __name__ == "__main__":
    main()

Notes:

  1. The regex can probably be better, but I'm no expert in that. (I use https://regexr.com/ to test)
  2. If you want, you can split main into several subfunctions. (eg. input, body extraction, header/footer eval)
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Welcome to the Code Review Community, while you have made two meaningful observation you may be missing the point of code review. This site is quite different from stack overflow. The goal here is to help the original poster improve their code, not re-write their code for them. It is far more important to provide explanations for why they should change their code and much less important to provide the correct code. You might want to read through How do I write a good answer?. A good answer might not contain any code. \$\endgroup\$
    – pacmaninbw
    Commented Dec 4, 2020 at 15:37
  • \$\begingroup\$ @Swedgin Thanks! I was trying to look at the documentation for the re but I found the syntax really confusing. One thing I guess I didn't go over well in my question is that the Day of the week Marker can appear anywhere in the signature, once a day. I didn't know := was a thing but after reading about it seems really useful. Thanks again! I'll use this to learn a bit more :> \$\endgroup\$
    – Ragov
    Commented Dec 4, 2020 at 17:01

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.