diff --git a/calendar/generate_dates/TeachingCalendar.py b/calendar/generate_dates/TeachingCalendar.py new file mode 100644 index 0000000..26881af --- /dev/null +++ b/calendar/generate_dates/TeachingCalendar.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +import calendar +import datetime + + +class IsoWeek(datetime.date): + """Datetime representing the Monday of an ISO-8601 week. + """ + pass + + +class TeachingWeek(datetime.date): + """Datetime representing the Monday of a teaching week. + """ + pass + + +class BreakWeek(datetime.date): + """Datetime representing the Monday of a break (e.g., mid-semester) week. + """ + pass + + +class TeachingCalendar(): + """This class generates teaching-related dates for a specific paper + offered in a specific teaching period of a specific year. If you + don't provide teaching period details, it generates dates for the + current ISO-8601 year. + """ + weeks = [] + lecture_offsets = [] + skipped_lectures = [] + mondays = [] + period = "iso" + year = datetime.date.today().year + + def __init__(self, year, period, paper, period_config=None, + paper_config=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.year = year + self.period = period + self.paper = paper + + # This is likely to end up with one week too many at the end, + # but that doesn't matter as there will never be any teaching + # during the last week of December! + cal = calendar.Calendar().yeardatescalendar(self.year, width=12) + + self.mondays = [week[0] for month in cal[0] for week in month] + self.mondays = list(dict.fromkeys(self.mondays)) + if period_config: + self.update_weeks(period_config["weeks"]) + else: + last_week = datetime.date(self.year, 12, 28).isocalendar()[1] + self.update_weeks([{"iso": [1, last_week]}]) + if paper_config: + self.update_lectures(paper_config["lectures"]) + + def week(self, week_type, start, end=None): + end = start if end is None else end + return [ + week_type(self.mondays[w].year, self.mondays[w].month, + self.mondays[w].day) + for w in range(start - 1, end)] + + def teaching_date(self, period, week, offset=0): + pass + + def update_weeks(self, week_list): + for w in week_list: + for t, r in w.items(): + if t == "teach": + self.weeks += self.week(TeachingWeek, r[0], r[1]) + elif t == "break": + self.weeks += self.week(BreakWeek, r[0], r[1]) + elif t == "iso": + self.weeks += self.week(IsoWeek, r[0], r[1]) + else: + print("WARNING: ignored unknown week type {t}.".format(t=t)) + + def update_lectures(self, lecture_list): + for l in lecture_list: + for t, v in l.items(): + if t == "offsets": + self.lecture_offsets = v + elif t == "skip": + self.skipped_lectures = v + else: + print("WARNING: ignored unknown lecture key {t}.".format(t=t)) + + def calendar(self): + period = {} + week_num = break_num = 0 + for w in self.weeks: + if isinstance(w, (TeachingWeek, IsoWeek)): + # Increment first so that break weeks have the + # same week number as the preceding teaching week. + # This ensures the keys are always chronologically + # sorted. Reset the break number for each new + # teaching week. + week_num += 1 + break_num = 0 + period[week_num] = w + elif isinstance(w, BreakWeek): + # Allow for up to 99 consecutive break weeks, + # which should be sufficient :). Should probably + # throw an exception if we exceed that. + break_num += 0.01 + period[week_num + break_num] = w + return period + + def lecture_dates(self): + dates = {} + lecture_num = 1 + teaching_weeks = [t for t in self.weeks if isinstance(t, TeachingWeek)] + for week_index, monday in enumerate(teaching_weeks): + for offset_index, offset in enumerate(self.lecture_offsets): + lec = week_index * 2 + offset_index + 1 + if lec in self.skipped_lectures: + continue + dates[lecture_num] = monday + datetime.timedelta(offset) + lecture_num += 1 + return dates