From 877e555432e14d602f8a805765b0d21dbc8c64c7 Mon Sep 17 00:00:00 2001
From: Matthew Hague <matthew.hague@rhul.ac.uk>
Date: Fri, 7 Jun 2024 10:40:08 +0100
Subject: [PATCH] add(timetable): read either CSV or Excel timetable files

---
 timetabling/timetable-ics.py | 144 +++++++++++++++++++----------------
 1 file changed, 78 insertions(+), 66 deletions(-)

diff --git a/timetabling/timetable-ics.py b/timetabling/timetable-ics.py
index 604eb16..493420e 100644
--- a/timetabling/timetable-ics.py
+++ b/timetabling/timetable-ics.py
@@ -1,5 +1,6 @@
 
 import csv
+import pandas as pd
 import pytz
 import re
 import sys
@@ -7,15 +8,16 @@ import sys
 from ics import Calendar, Event
 from datetime import datetime, timedelta
 
-from typing import Generator
+from typing import Dict, Generator
 
 BASE_DATE = datetime(2024, 9, 23, 0, 0, 0, 0, pytz.timezone("Europe/London"))
 BEDFORD = 100 # not a year
 
 if len(sys.argv) < 2:
     print("Usage: timetable-ics.py timetable1.csv [timetable2.csv..]")
+    print("May also pass excel files instead of csv")
 
-timetable_csvs = sys.argv[1:]
+timetable_files = sys.argv[1:]
 
 cals = { year: Calendar() for year in range(0, 6) }
 cals.update({ BEDFORD : Calendar() })
@@ -62,70 +64,80 @@ def get_end_time(start : datetime, duration : str) -> datetime:
     hours = int(duration.split(":")[0])
     return start + timedelta(hours=hours)
 
-for csvfile in timetable_csvs:
-    with open(csvfile) as f:
-        for row in csv.DictReader(f):
-            day = None
-            start = None
-            name = row["Name"]
-            if "Suggested Days" in row:
-                day = row["Suggested Days"]
-            elif "Scheduled Days" in row:
-                day = row["Scheduled Days"]
-            elif "Day(s)" in row:
-                day = row["Day(s)"]
-            if "Suggested Time" in row:
-                start = row["Suggested Time"]
-            elif "Scheduled Start Time" in row:
-                start = row["Scheduled Start Time"]
-            elif "Start Time" in row:
-                start = row["Start Time"]
-            duration_hours = row["Duration"]
-            weeks = get_weeks(row["Teaching Week Pattern"])
-
-            location = ""
-            if "PROVISIONAL Location Name" in row:
-                location = row["PROVISIONAL Location Name"]
-            if "Required Location Name" in row:
-                location = row["PROVISIONAL Location Name"]
-
-            if day is None:
-                print("Could not find timetabled day column, aborting")
-                exit()
-
-            if start is None:
-                print("Could not find timetabled start time column, aborting")
-                exit()
-
-            if "TUT" in name:
-                continue
-
-            for week in weeks:
-                offset = get_day_offset(day)
-                if offset < 0:
-                    print(f"Warning: ignoring day {day} of {row}")
-                else:
-                    start_time = get_time(week, offset, start)
-                    end_time = get_end_time(start_time, duration_hours)
-                    event = Event()
-                    event.name = name
-                    event.begin = start_time
-                    event.end = end_time
-                    for year in get_years(name):
-                        cals[year].events.add(event)
-                    if "bedford" in location.lower():
-                        if "04-06" in location:
-                            event.name = f"ALL {name}"
-                        elif "04" in location:
-                            event.name = f"4 {name}"
-                        elif "05" in location:
-                            event.name = f"5 {name}"
-                        elif "06" in location:
-                            event.name = f"6 {name}"
-                        else:
-                            event.name = f"? {name}"
-
-                        cals[BEDFORD].events.add(event)
+def iter_rows(ttfile : str) -> Generator[Dict[str, str], None, None]:
+    """Iterate over rows of csv or xlsx file as dictionary"""
+    if ttfile.lower().endswith(".csv"):
+        with open(ttfile) as f:
+            for row in csv.DictReader(f):
+                yield row
+    else: # try any excel
+        df = pd.read_excel(ttfile, dtype=str, na_filter=False)
+        for row in df.to_dict(orient="records"):
+            yield row
+
+for ttfile in timetable_files:
+    for row in iter_rows(ttfile):
+        day = None
+        start = None
+        name = row["Name"]
+        if "Suggested Days" in row:
+            day = row["Suggested Days"]
+        elif "Scheduled Days" in row:
+            day = row["Scheduled Days"]
+        elif "Day(s)" in row:
+            day = row["Day(s)"]
+        if "Suggested Time" in row:
+            start = row["Suggested Time"]
+        elif "Scheduled Start Time" in row:
+            start = row["Scheduled Start Time"]
+        elif "Start Time" in row:
+            start = row["Start Time"]
+        duration_hours = row["Duration"]
+        weeks = get_weeks(row["Teaching Week Pattern"])
+
+        location = ""
+        if "PROVISIONAL Location Name" in row:
+            location = row["PROVISIONAL Location Name"]
+        if "Required Location Name" in row:
+            location = row["PROVISIONAL Location Name"]
+
+        if day is None:
+            print("Could not find timetabled day column, aborting")
+            exit()
+
+        if start is None:
+            print("Could not find timetabled start time column, aborting")
+            exit()
+
+        if "TUT" in name:
+            continue
+
+        for week in weeks:
+            offset = get_day_offset(day)
+            if offset < 0:
+                print(f"Warning: ignoring day {day} of {row}")
+            else:
+                start_time = get_time(week, offset, start)
+                end_time = get_end_time(start_time, duration_hours)
+                event = Event()
+                event.name = name
+                event.begin = start_time
+                event.end = end_time
+                for year in get_years(name):
+                    cals[year].events.add(event)
+                if "bedford" in location.lower():
+                    if "04-06" in location:
+                        event.name = f"ALL {name}"
+                    elif "04" in location:
+                        event.name = f"4 {name}"
+                    elif "05" in location:
+                        event.name = f"5 {name}"
+                    elif "06" in location:
+                        event.name = f"6 {name}"
+                    else:
+                        event.name = f"? {name}"
+
+                    cals[BEDFORD].events.add(event)
 
 for year in cals:
     if year != BEDFORD:
-- 
GitLab