from typing import List, Sequence from .education_timetable_clash_types import TimetableClash, TimetableSession def _is_int(value: object) -> bool: return isinstance(value, int) and not isinstance(value, bool) def _shared(a: Sequence[str], b: Sequence[str]) -> List[str]: out: List[str] = [] for x in a: if x in b and x not in out: out.append(x) return out def timetable_clashes(sessions: Sequence[TimetableSession]) -> List[TimetableClash]: """Every pair of sessions that overlap on the same day and share a room, teacher or group. Times are half-open: a session ending at 600 does not clash with one starting at 600.""" ids = set() for s in sessions: if s.id in ids: raise ValueError('session id "%s" appears twice' % (s.id,)) ids.add(s.id) if not _is_int(s.day) or s.day < 1: raise ValueError('day must be a whole number of 1 or more, received %s for "%s"' % (s.day, s.id)) if not _is_int(s.start) or s.start < 0 or s.start > 1439: raise ValueError('start must be a whole minute from 0 to 1439, received %s for "%s"' % (s.start, s.id)) if not _is_int(s.end) or s.end <= s.start or s.end > 1440: raise ValueError( 'end must be a whole minute after start and no later than 1440, received %s for "%s"' % (s.end, s.id) ) found = [] for i in range(len(sessions)): for j in range(i + 1, len(sessions)): a = sessions[i] b = sessions[j] if a.day != b.day: continue start = max(a.start, b.start) end = min(a.end, b.end) if start >= end: continue rooms = _shared(a.rooms, b.rooms) teachers = _shared(a.teachers, b.teachers) groups = _shared(a.groups, b.groups) if len(rooms) + len(teachers) + len(groups) == 0: continue clash = TimetableClash( first=a.id, second=b.id, day=a.day, start=start, end=end, rooms=rooms, teachers=teachers, groups=groups ) found.append((a.day, start, i, j, clash)) found.sort(key=lambda f: (f[0], f[1], f[2], f[3])) return [f[4] for f in found]