4
\$\begingroup\$

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...')
```
Mast
13.8k12 gold badges56 silver badges127 bronze badges
asked May 12, 2020 at 1:38
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$
  • Not a fan of all your single or double letter variable names. Please write out variables like time, datetime and timedelta.

    However I do think s is more readable than a written out variable name.

  • I would prefer it if decode_time copied t 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 ifs in decode_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 prints you have. These have side effects like deleting the type in decode_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.
answered May 12, 2020 at 3:09
\$\endgroup\$
2
  • \$\begingroup\$ The problem with datetime is its alternatives. from datetime import datetime will be confusing, since now the datetime class takes the place of what is usually the datetime module. This leaves two sensible, as in unambiguous/clear, ways. Either import datetime and call datetime.datetime everywhere, or the somewhat widespread short verion from datetime import datetime as dt. Subjective, but the latter is useful (like np to numpy). \$\endgroup\$ Commented May 12, 2020 at 7:03
  • 1
    \$\begingroup\$ @AlexPovel Yeah no, consistent code fixes that problem. \$\endgroup\$ Commented May 12, 2020 at 9:47

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.