This is a simple command line interface program that just keeps track of duration spent on multiple tasks.
Any areas of improvements are welcomed to be pointed out.
import json
import datetime
dt = datetime.datetime
td = datetime.timedelta
def encode_time(t):
if isinstance(t, dt):
return {
'_type': 'datetime',
'year': t.year,
'month': t.month,
'day': t.day,
'hour': t.hour,
'minute': t.minute,
'second': t.second
}
elif isinstance(t, td):
return {
'_type': 'timedelta',
'days': t.days,
'seconds': t.seconds,
'microseconds': t.microseconds
}
return t
def decode_time(t):
if '_type' in t:
data_type = t['_type']
del t['_type']
if data_type == 'datetime':
return dt(**t)
elif data_type == 'timedelta':
return td(**t)
return t
def display_pending():
"""
pretty prints the pending dictionary.
:return: None
"""
print('\n')
if pending:
max_len = max([len(key) for key in pending.keys()])
print('Pending Activities:')
print('-'*40)
else:
max_len = 0
print('No pending data.')
counter = 0
for key, value in pending.items():
duration_so_far = (dt.now() - value)
seconds = duration_so_far.seconds
days = duration_so_far.days
hours = seconds // 3600
minutes = (seconds // 60) % 60
s = 's' if days > 1 else ''
days = f'{days} Day{s} ' if days > 0 else ''
s = 's' if hours > 1 else ''
hours = f'{hours} Hour{s} ' if hours > 0 else ''
s = 's' if minutes > 1 else ''
minutes = f'{minutes} Min{s} ' if minutes > 0 else ''
seconds = f'{seconds} Seconds' if seconds < 60 else ''
print(f'[{counter}] {key.capitalize():{max_len}} | {value}') # Key and start time.
max_len += 4 # Adding 4 to max_len to make sure this line is aligned with the one above it.
print(f'{"." * max_len:{max_len}} | {days}{hours}{minutes}{seconds}\n') # Duration so far.
print('-' * 40)
max_len -= 4 # Avoiding mutating max_len.
counter += 1
def display_durations():
"""
Pretty prints the durations dictionary.
:return: None
"""
print('\n')
counter = 0
if durations:
max_len = max([len(key) for key in durations.keys()])
print('Durations: ')
print('_' * 40)
else:
max_len = 0
print('No durations data')
for key, value in durations.items():
print(f'[{counter}] {key.capitalize():{max_len}} | {value}')
print('-' * 40)
counter += 1
pending = {}
durations = {}
# Reading data from pending.json
try:
with open('pending.json', 'r') as pending_json:
pending = json.load(pending_json, object_hook=decode_time)
s = 's' if len(pending) > 1 else ''
print(f'{len(pending)} pending item{s} loaded from disk')
except FileNotFoundError:
print(' "pending.json" was not found, creating.')
open('pending.json', 'x').close()
except json.decoder.JSONDecodeError:
print('pending.json is empty...')
# Reading data from durations.json
try:
with open('durations.json', 'r') as durations_json:
durations = json.load(durations_json, object_hook=decode_time)
s = 's' if len(durations) > 1 else ''
print(f'{len(durations)} duration item{s} loaded from disk')
except FileNotFoundError:
print(' "durations.json" was not found, creating.')
open('durations.json', 'x').close()
except json.decoder.JSONDecodeError:
print('durations.json is empty...')
if pending:
display_pending()
if durations:
display_durations()
# Acquiring user input.
while True:
activity = input('\n>>> ').lower()
now = dt.now()
start_time = pending.get(activity, None)
if activity == 'quit':
print('Exiting.')
break
elif activity == '':
continue
if activity in pending:
duration = now - start_time
print(f'"{activity}" ended. Duration: {duration}')
durations[activity] = durations.get(activity, td(0)) + duration # Record duration of activity.
del pending[activity] # Delete activity from pending list to confirm that it's completed.
continue
elif activity == 'man':
activity = input('Activity Name: ')
activity_duration = {}
# Get num of days, hours, etc.. of manually eneterd activity.
for parameter in ['days', 'hours', 'minutes', 'seconds', 'microseconds']:
while True:
i = input(f'{parameter.capitalize()}: ')
if i.isnumeric():
activity_duration[parameter] = int(i)
break
elif i == '':
activity_duration[parameter] = 0
break
add_minus = input('Add / Minus: ').lower()
if add_minus == 'add':
durations[activity] = durations.get(activity, td(0)) + td(**activity_duration)
elif add_minus == 'minus':
durations[activity] = durations.get(activity, td(0)) - td(**activity_duration)
display_durations()
continue
elif activity == 'del':
activity = input('Delete: ')
if activity == 'all':
confirmed = input('Delete Everything? y/n ')
if confirmed == 'y':
pending.clear()
durations.clear()
print('Data Cleared.')
continue
key_index = [[None, None]] # A list of index, key pairs for each key in pending or durations dictionaries.
# If the activity the user wants to delete is a number, treat it as an index,
# Unless the activity is an entry in either pending/durations lists:
if activity.isnumeric() and activity not in set(list(pending.keys()) + list(durations.keys())):
is_numeric = True
activity = int(activity)
wanted_list = input('Delete From Pending/Durations? p/d: ')
if wanted_list == 'd':
key_index = [(index, key) for index, key in enumerate(durations.keys())]
elif wanted_list == 'p':
key_index = [(index, key) for index, key in enumerate(pending.keys())]
# If no list specified then delete from durations or pending according to index given.
else:
if activity <= len(durations) - 1:
key_index = [(index, key) for index, key in enumerate(durations.keys())]
elif activity <= len(pending) - 1:
key_index = [(index, key) for index, key in enumerate(pending.keys())]
for index, key in key_index:
if index == activity:
break
activity = key
else:
is_numeric = False
if activity in pending:
not_in_pending = False
del pending[activity]
print(f'"{activity.capitalize()}" deleted from pending')
else:
not_in_pending = True
if activity in durations:
not_in_durations = False
del durations[activity]
print(f'{activity.capitalize()} deleted from durations')
else:
not_in_durations = True
if not_in_pending and not_in_durations:
if is_numeric:
print('No data')
else:
print('Key Not Found')
continue
elif activity == 'data':
display_pending()
display_durations()
continue
elif activity == 'help':
print('''
Enter activity name for it to be started.
Enter the same name again for it to be ended and record it's duration.
Commands:
man: manually edit a duration
del: delete an entry from pending activities and/or duration
data: show currently pending activities and duration records
quit: exit program and save edits.
''')
continue
pending[activity] = now
print(f'"{activity.capitalize()}" started on: {now}')
print(' Writing updated data to disk.')
if len(pending) > 0:
with open('pending.json', 'w') as pending_json:
json.dump(pending, pending_json, default=encode_time, indent=2)
else:
# So that json doesn't dump an empty dictionary symbol: {} to the file.
open('pending.json', 'w').close()
if len(durations) > 0:
with open('durations.json', 'w') as durations_json:
json.dump(durations, durations_json, default=encode_time, indent=2)
else:
open('durations.json', 'w').close()
print(' Data updated.')
exit_confirm = input('Press Enter To Exit...')
```
1 Answer 1
Not a fan of all your single or double letter variable names. Please write out variables like
time
,datetime
andtimedelta
.However I do think
s
is more readable than a written out variable name.I would prefer it if
decode_time
copiedt
before mutating it. This makes your code easier to use as there are no side effects.t = t.copy() data_type = t.pop('_type')
You can change the
if
s indecode_time
to use a dictionary to make the code easier to extend.If you have more than two data types then the dictionary will allow you to reduce duplicate code.
You can use either of the following based on your preference.
DECODES = { 'datetime': datetime.datetime, 'timedelta': datetime.timedelta, }
try: fn = DECODES[data_type] except KeyError: pass else: return fn(**t)
return DECODES.get(data_type, lambda **kwargs: kwargs)(**t)
You kinda just gave up on seconds in
display_pending
. You've not calculated how many seconds it would need nor have you adjusted for when it's 1.To reduce the amount of duplication you can use a fairly simple for loop and a dictionary. We can build the dictionary with the existing code to build the variables. If we assign the values to the name to print, we can iterate over the dictionary getting all the information needed to display each unit.
With some carefully placed assignments we can exploit the fact that dictionaries are sorted and have the same output you currently have.
Additionally we can use a comprehension for some sugar to filter missing units.
duration = {'Day': duration_so_far.days} remainder, seconds = divmod(duration_so_far.seconds, 60) duration['Hour'], duration['Minute'] = divmod(remainder, 60) duration['Second'] = seconds time = ' '.join( f'{v} {k}{"s" if v != 1 else ""}' for k, v in duration.items() if v ) ... print(f'{"." * max_len:{max_len}} | {time}\n')
I would recommend toning down the amount of
print
s you have. These have side effects like deleting the type indecode_time
. This makes your code harder to:- Test - now you need to wrap
sys.std.out
for basic functions. - Understand - others now have to second guess each and every one of your functions. This is because you've done it once and not been explicit about it, what's to stop you from doing it twice?
- Maintain - refactoring your code to keep the same output is more challenging without a complete rewrite.
- Test - now you need to wrap
-
\$\begingroup\$ The problem with
datetime
is its alternatives.from datetime import datetime
will be confusing, since now thedatetime
class takes the place of what is usually thedatetime
module. This leaves two sensible, as in unambiguous/clear, ways. Eitherimport datetime
and calldatetime.datetime
everywhere, or the somewhat widespread short verionfrom datetime import datetime as dt
. Subjective, but the latter is useful (likenp
tonumpy
). \$\endgroup\$Alex Povel– Alex Povel2020年05月12日 07:03:53 +00:00Commented May 12, 2020 at 7:03 -
1\$\begingroup\$ @AlexPovel Yeah no, consistent code fixes that problem. \$\endgroup\$2020年05月12日 09:47:36 +00:00Commented May 12, 2020 at 9:47