First commit

This commit is contained in:
Juhani Haverinen 2016-08-24 15:12:52 +03:00
commit 1692a5b23b
8 changed files with 402 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.zip
lukkari.py
__pycache__
*.pyc

16
Makefile Normal file
View File

@ -0,0 +1,16 @@
all: lukkari.py
lukkari.py: lukkari/*.py
zip --quiet lukkari lukkari/*.py
zip --quiet --junk-paths lukkari lukkari/__main__.py
echo '#!/usr/bin/env python3' > lukkari.py
cat lukkari.zip >> lukkari.py
rm lukkari.zip
chmod +x lukkari.py
.PHONY: all clean
clean:
find . -name "*.pyc" -delete
find . -name "__pycache__" -delete
rm lukkari.zip lukkari.py || true

69
lukkari/__init__.py Normal file
View File

@ -0,0 +1,69 @@
import datetime
import sys
import os.path
from . import check_date
from . import daterange
from . import generate_timetable
from . import parse_coursefile
def parse_date(text):
split = text.split('-')
assert(len(split) == 3)
return tuple(map(int, split))
def main():
if len(sys.argv) == 2:
year, week, day = datetime.date.today().isocalendar()
if day >= 6: # on weekend, show next week
year, week, day = (datetime.date.today() + datetime.timedelta(2)).isocalendar()
dates = daterange.week(year, week)
filename = sys.argv[1]
elif len(sys.argv) == 3:
date = parse_date(sys.argv[1])
dates = daterange.between(date, date)
filename = sys.argv[2]
elif len(sys.argv) == 4:
start = parse_date(sys.argv[1])
end = parse_date(sys.argv[2])
dates = daterange.between(start, end)
filename = sys.argv[3]
else:
print('%s [start [end]] file' % (os.path.basename(sys.argv[0])))
print('start and end are in yyyy-mm-dd format')
sys.exit(1)
with open(filename, 'r') as f:
courses_parsed = parse_coursefile.parse(f.read())
courses = []
for name, info, parsed_filter in courses_parsed:
date_filter = check_date.compile(parsed_filter)
courses.append((name, info, date_filter))
timetable = generate_timetable.generate_timetable(dates, courses)
timetable_by_date = []
current_date = None
current_date_entries = []
for date, name, info in timetable:
if current_date is None:
current_date = date
if date == current_date:
current_date_entries.append((name, info))
else:
timetable_by_date.append((current_date, current_date_entries))
current_date = date
current_date_entries = []
current_date_entries.append((name, info))
if current_date is not None and current_date_entries != []:
timetable_by_date.append((current_date, current_date_entries))
print(dates)
print()
for date, entries in timetable_by_date:
print(date)
for name, info in entries:
print('\t%s: %s' % (name, info))

3
lukkari/__main__.py Normal file
View File

@ -0,0 +1,3 @@
import lukkari
if __name__ == '__main__':
lukkari.main()

94
lukkari/check_date.py Normal file
View File

@ -0,0 +1,94 @@
import enum
from . import daterange
class Filters(enum.Enum):
in_date_range, is_weekday = range(2)
class Conjunctions(enum.Enum):
all, any, none, implies = range(4)
def compile(parsed):
assert(len(parsed) >= 1)
function, *parameters = parsed
assert(function in ['date', 'week', 'weekday', 'and', 'or', 'not', 'if'])
if function == 'date':
if len(parameters) == 1:
start, = parameters
end = start
else:
assert(len(parameters) == 2)
start, end = parameters
date_range = daterange.between(start, end)
return (Filters.in_date_range, date_range)
elif function == 'week':
if len(parameters) > 1:
return (Conjunctions.any, *[compile(('week', week)) for week in parameters])
else:
assert(len(parameters) == 1)
week_data, = parameters
year, week = week_data
date_range = daterange.week(year, week)
return (Filters.in_date_range, date_range)
elif function == 'weekday':
if len(parameters) > 1:
return (Conjunctions.any, *[compile(('weekday', weekday)) for weekday in parameters])
else:
assert(len(parameters) == 1)
weekday, = parameters
return (Filters.is_weekday, weekday)
elif function == 'and':
return (Conjunctions.all, *[compile(parameter) for parameter in parameters])
elif function == 'or':
return (Conjunctions.any, *[compile(parameter) for parameter in parameters])
elif function == 'not':
return (Conjunctions.none, *[compile(parameter) for parameter in parameters])
elif function == 'if':
assert(len(parameters) == 2)
condition, then = parameters
return (Conjunctions.implies, compile(condition), compile(then))
def check_day_match(day, date_filter):
assert(len(date_filter) >= 1)
function, *parameters = date_filter
assert(function in Filters or function in Conjunctions)
if function == Conjunctions.implies:
assert(len(parameters) == 2)
elif function in [Filters.in_date_range, Filters.is_weekday]:
assert(len(parameters) == 1)
else:
assert(function in [Conjunctions.all, Conjunctions.any, Conjunctions.none])
if function == Conjunctions.all:
return all(map(lambda parameter: check_day_match(day, parameter), parameters))
elif function == Conjunctions.any:
return any(map(lambda parameter: check_day_match(day, parameter), parameters))
elif function == Conjunctions.implies:
left, right = parameters
left_truth = check_day_match(day, left)
right_truth = check_day_match(day, right)
return left_truth and right_truth or not left_truth
elif function == Conjunctions.none:
return not any(map(lambda parameter: check_day_match(day, parameter), parameters))
elif function == Filters.in_date_range:
date_range, = parameters
return day in date_range
elif function == Filters.is_weekday:
weekday, = parameters
return day.isoweekday() == weekday

67
lukkari/daterange.py Normal file
View File

@ -0,0 +1,67 @@
import datetime
class Daterange:
def __init__(self, start, length):
self.start = start
self.length = length
def range(self):
end = self.start + self.length - datetime.timedelta(1)
return (self.start, end)
def __contains__(self, day):
delta = day - self.start
return datetime.timedelta(0) <= delta and delta < self.length
def overlaps(self, other):
if other.start < self.start:
return other.overlaps(self)
assert(self.start <= other.start)
return other.start < self.start + self.length
def __repr__(self):
return 'Daterange(%s, %s)' % (repr(self.start), repr(self.length))
def __str__(self):
start, end = map(str, self.range())
return '%s - %s' % (start, end)
def __eq__(self, other):
return self.start == other.start and self.length == other.length
def __ne__(self, other):
return not self == other
def week(year, week):
# Ensure that week is correct for the given year
assert(1 <= week and week <= 53)
if week == 53:
assert(datetime.date(year, 12, 31).isocalendar()[1] == 53)
# First attempt, this will most likely be wrong
# Use 28 instead of probably better 30 so we don't have to special-case february
days_into_year = 1 + week * 7
guess = datetime.date(year + days_into_year // 365, days_into_year // 28 % 12 + 1, week * 7 % 28 + 1)
guess_year, guess_week, guess_weekday = guess.isocalendar()
while guess_year != year or guess_week != week or guess_weekday != 1:
year_delta = year - guess_year
week_delta = week - guess_week
weekday_delta = 1 - guess_weekday
# Year is not quite right, but by repeated application year_delta should become 0, after which it's correct
delta = datetime.timedelta(year_delta * 365 + week_delta * 7 + weekday_delta)
guess += delta
guess_year, guess_week, guess_weekday = guess.isocalendar()
return Daterange(guess, datetime.timedelta(7))
def between(start, end):
assert(len(start) == 3 and len(end) == 3)
start_year, start_month, start_day = start
end_year, end_month, end_day = end
start_obj = datetime.date(start_year, start_month, start_day)
end_obj = datetime.date(end_year, end_month, end_day)
return Daterange(start_obj, end_obj - start_obj + datetime.timedelta(1))

View File

@ -0,0 +1,18 @@
import datetime
from . import check_date
def generate_timetable(day_range, courses):
start_date, end_date = day_range.range()
date = start_date
appointments = []
while True:
for name, info, date_filter in courses:
if check_date.check_day_match(date, date_filter):
appointments.append((date, name, info))
if date == end_date:
break
date += datetime.timedelta(1)
return appointments

131
lukkari/parse_coursefile.py Normal file
View File

@ -0,0 +1,131 @@
def strip_comments(text):
lines = []
for line in text.split('\n'):
comment_start = line.find('#')
if comment_start != -1:
lines.append(line[:comment_start])
else:
lines.append(line)
return '\n'.join(lines)
def parse_filter(text):
weekdays = {'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, 'fri': 5, 'sat': 6, 'sun': 7}
functions = ['date', 'week', 'weekday', 'and', 'or', 'not', 'if']
def eof():
nonlocal index, length
return index >= length
def skip_whitespace():
nonlocal index, length
while not eof() and text[index].isspace():
index += 1
def match(matches):
nonlocal index, length
assert(not eof())
assert(text[index] in matches)
index += 1
def read_atom():
nonlocal index, length
start = index
while not eof() and not text[index].isspace() and text[index] != ')':
index += 1
return text[start:index]
def subexpression():
nonlocal index, length
if not eof() and text[index] == '(':
match('(')
skip_whitespace()
elements = []
function = read_atom()
skip_whitespace()
assert(not eof())
assert(function in functions)
elements.append(function)
while not eof() and text[index] != ')':
elements.append(subexpression())
skip_whitespace()
match(')')
return elements
else:
atom = read_atom()
assert(len(atom) >= 1)
if atom in weekdays:
return weekdays[atom]
else:
split = atom.split('-')
assert(all(map(lambda x: x.isnumeric(), split)))
assert(len(split) == 2 or len(split) == 3)
return tuple(map(int, split))
index = 0
length = len(text)
skip_whitespace()
expression = subexpression()
skip_whitespace()
assert(eof())
return expression
def parse(text):
def eof():
nonlocal index, length
return index >= length
def skip_whitespace():
nonlocal index, length
while not eof() and text[index].isspace():
index += 1
def match(matches):
nonlocal index, length
assert(not eof())
assert(text[index] in matches)
index += 1
def read_field():
nonlocal index, length
skip_whitespace()
start = index
while not eof() and text[index] not in [';', '\n']:
if text[index] == '\\':
index += 1
index += 1
return text[start:index].strip()
text = strip_comments(text)
index = 0
length = len(text)
courses = []
while not eof():
name = read_field()
match(';')
info = read_field()
match(';')
date_filter = parse_filter(read_field())
if not eof():
match('\n')
courses.append((name, info, date_filter))
assert(eof())
return courses