Created
September 5, 2014 01:08
-
-
Save bbengfort/bd5c023809daaf077737 to your computer and use it in GitHub Desktop.
Needlessly complex course date generation tool.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# coursenotes | |
# A helper to come up with Course Notes Titles | |
# | |
# Author: Benjamin Bengfort <benjamin@bengfort.com> | |
# Created: Thu Sep 04 15:00:55 2014 -0400 | |
# | |
# Copyright (C) 2014 Bengfort.com | |
# For license information, see LICENSE.txt | |
# | |
# ID: coursenotes.py [] benjamin@bengfort.com $ | |
""" | |
A helper to come up with Course Notes Titles. | |
Evernote course notes are titled as follows: | |
Course Title Day [Human Day]: Day Month Year | |
But ocassionally these can be hard to remember on a per-semester basis, | |
so I've created this small utility to generate the titles for a course for | |
an entire semester given a start and end date! | |
""" | |
########################################################################## | |
## Imports | |
########################################################################## | |
import sys | |
import json | |
import argparse | |
import humanize | |
from datetime import date, timedelta | |
from dateutil import parser as dtparser | |
########################################################################## | |
## Module Constants | |
########################################################################## | |
## Program descriptor | |
PROG = { | |
"version": "1.0", | |
"description": "A helper to come up with Course Notes titles", | |
"epilog": "This was done in procrastination" | |
} | |
## Weekday codes | |
DAYS = { | |
'M': 'Monday', | |
'T': 'Tuesday', | |
'W': 'Wednesday', | |
'R': 'Thursday', | |
'F': 'Friday', | |
'S': 'Saturday', | |
'U': 'Sunday', | |
} | |
## Indices for the days | |
DAYIDX = "MTWRFSU" | |
## Standard Date format | |
DATEFORMAT = "%d %b %Y" | |
DEBUGFORMAT = "%a %d %b %Y" | |
## Humanize numbers | |
TEENS = ('ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen') | |
DECADES = (None, None, 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety') | |
########################################################################## | |
## Helper Functions | |
########################################################################## | |
def weekday(dt): | |
lookup = dict(zip(DAYS.values(), DAYS.keys())) | |
return lookup[dt.strftime("%A")] | |
########################################################################## | |
## The Worker Module | |
########################################################################## | |
class Course(object): | |
""" | |
A Course defines the instances and properties of a particular course. | |
""" | |
def __init__(self, name, start, **kwargs): | |
self.name = name # The name of the course | |
self.start = start # The start date of the course | |
self.end = kwargs.pop('end', None) # The end date of the course | |
self.skip = set(kwargs.pop('skip', [])) # A list of dates to skip (holidays) | |
self.days = kwargs.pop('days', 'MTWRF') # The days of the week the course is held | |
self.times = kwargs.pop('times', None) # The number of times the course occurs | |
self.dtfmt = kwargs.pop('datefmt', DATEFORMAT) | |
###/////////////////////////////////////////////////////////////////// | |
## Properties | |
###/////////////////////////////////////////////////////////////////// | |
@property | |
def start(self): | |
return self._start | |
@start.setter | |
def start(self, value): | |
if isinstance(value, basestring): | |
value = dtparser.parse(value).date() | |
self._start = value | |
@property | |
def end(self): | |
if self._end is None and self._times is not None: | |
# Compute the end date from the times | |
self._end = self._end_from_times() | |
return self._end | |
@end.setter | |
def end(self, value): | |
if isinstance(value, basestring): | |
value = dtparser.parse(value).date() | |
self._end = value | |
@property | |
def times(self): | |
if self._times is None: | |
if self._end is not None: | |
# Compute the times from the end date | |
self._times = self._times_from_end() | |
else: | |
# Just set a default number of times - 10 | |
self._times = 10 | |
return self._times | |
@times.setter | |
def times(self, value): | |
self._times = value | |
@property | |
def skip(self): | |
return self._skip | |
@skip.setter | |
def skip(self, value): | |
value = set([dtparser.parse(v).date() for v in value]) | |
self._skip = value | |
@property | |
def days(self): | |
return self._days | |
@days.setter | |
def days(self, value): | |
value = value.upper() | |
if len(value) > 7: | |
raise Exception("Cannot specify more days than there are in the week!") | |
if len(set(value)) != len(value): | |
raise Exception("Must specify unique days, in week order!") | |
for char in value: | |
if char not in DAYIDX: | |
raise Exception("Unknown weekday '%s' use '%s' for day representation" % (char, DAYIDX)) | |
self._days = value | |
###/////////////////////////////////////////////////////////////////// | |
## Internal methods | |
###/////////////////////////////////////////////////////////////////// | |
def _end_from_times(self): | |
weeks = (self.times / len(self.days)) + 1 | |
return self.start + timedelta(weeks=weeks) | |
def _times_from_end(self): | |
delta = self.end - self.start | |
weeks = (delta.days / 7) + 1 | |
return weeks * len(self.days) | |
###/////////////////////////////////////////////////////////////////// | |
## Public Methods | |
###/////////////////////////////////////////////////////////////////// | |
def humanize(self, idx): | |
idx += 1 # Deal with 0-index | |
if idx < 10: | |
return humanize.apnumber(idx).title() | |
if idx < 20: | |
return TEENS[idx % 10].title() | |
if idx > 19: | |
pre = DECADES[idx / 10] | |
pos = humanize.apnumber(idx % 10) | |
num = " ".join((pre, pos)) if pos != "0" else pre | |
return num.title() | |
def interval(self): | |
""" | |
Returns a dict of how many days are between classes based on the | |
days provided. E.g. the interval for "MWF" is keyed as follows: | |
{ | |
"M": 2, | |
"T": None, | |
"W": 2, | |
"R": None, | |
"F": 3, | |
"S": None, | |
"U": None | |
} | |
Which indicates how many days to skip for each date. If you land | |
on a date which has a None interval, you've probably errored. | |
""" | |
vals = dict(zip(DAYIDX, (None,)*len(DAYIDX))) | |
# On a single day, the index is always 7 | |
if len(self.days) == 1: | |
vals[self.days] = 7 | |
return vals | |
for idx, day in enumerate(self.days): | |
nxt = self.days[idx+1] if idx+1 < len(self.days) else self.days[0] | |
iday = DAYIDX.find(day) | |
inxt = DAYIDX.find(nxt) | |
delta = inxt - iday if inxt > iday else inxt + len(DAYIDX) - iday | |
vals[day] = delta | |
return vals | |
def dates(self): | |
""" | |
Yields every single date that is available for a course | |
""" | |
interval = self.interval() | |
current = self.start | |
if weekday(current) not in self.days: | |
for idx in xrange(6): | |
current = current + timedelta(days=1) | |
if weekday(current) in self.days: | |
break | |
for x in xrange(self.times): | |
if current not in self.skip and current < self.end: | |
yield current | |
delta = timedelta(days=interval[weekday(current)]) | |
current += delta | |
def __iter__(self): | |
for idx, date in enumerate(self.dates()): | |
yield "%s Day %s: %s" % (self.name, self.humanize(idx), date.strftime(self.dtfmt)) | |
########################################################################## | |
## Main Function | |
########################################################################## | |
def main(*argv): | |
""" | |
Implements the command line parsing and constructs the course notes. | |
""" | |
# Construct the parser | |
parser = argparse.ArgumentParser(**PROG) | |
parser.add_argument('name', nargs=1, help='The name of the course') | |
parser.add_argument('start', nargs=1, help='The start date of the course') | |
parser.add_argument('--end', help='The end date of the course') | |
parser.add_argument('--skip', nargs="*", help='A list of dates to skip') | |
parser.add_argument('--days', help='The days of the week the course is held') | |
parser.add_argument('--times', type=int, help='The number of times the course occurs') | |
parser.add_argument('--datefmt', help='Specify the dateformat for the course') | |
# Parse the arguments | |
args = dict(vars(parser.parse_args())) | |
# Remove any None values | |
for key in args.keys(): | |
if args[key] is None: | |
del args[key] | |
# Deal with list arguments | |
for key in ('name', 'start'): | |
args[key] = args[key][0] | |
# Implement the functionality | |
try: | |
course = Course(**args) | |
for date in course: | |
print date | |
parser.exit(0) | |
except Exception as e: | |
parser.error(str(e)) | |
if __name__ == '__main__': | |
main(*sys.argv[1:]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment