Skip to content

Instantly share code, notes, and snippets.

@bbengfort
Created September 5, 2014 01:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bbengfort/bd5c023809daaf077737 to your computer and use it in GitHub Desktop.
Save bbengfort/bd5c023809daaf077737 to your computer and use it in GitHub Desktop.
Needlessly complex course date generation tool.
#!/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