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!
1 Answer 1
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:
- The regex can probably be better, but I'm no expert in that. (I use https://regexr.com/ to test)
- If you want, you can split
maininto several subfunctions. (eg. input, body extraction, header/footer eval)
-
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\$2020年12月04日 15:37:25 +00:00Commented 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– Ragov2020年12月04日 17:01:20 +00:00Commented Dec 4, 2020 at 17:01
str.split()to split strings. Eg.[header, signature, footer] = usr_str.split('..')\$\endgroup\$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\$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\$