From 0e9457c6c454fe449f0856194b89e8bc87214783 Mon Sep 17 00:00:00 2001 From: Theo Wiese <theo.wiese@student.reutlingen-university.de> Date: Thu, 8 May 2025 14:16:55 +0200 Subject: [PATCH] Improve lesson merging algorithm The algorithm now takes into account simultaneous lessons of the same lecture which have overlapping rooms or the same set of teachers --- lib/timetable/timetable.dart | 5 +- lib/untis/lesson.dart | 40 +++++++-- test/lesson_merge_test.dart | 154 +++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 test/lesson_merge_test.dart diff --git a/lib/timetable/timetable.dart b/lib/timetable/timetable.dart index 33eea00..bb0d2b6 100644 --- a/lib/timetable/timetable.dart +++ b/lib/timetable/timetable.dart @@ -153,8 +153,9 @@ class _TimetableState extends State<Timetable> { list.removeAt(i - 1); list.insert(i - 1, merged); i--; // Because the list is shortened, the counter stays the same - } on StateError catch (_) { - print("Same lecture lessons can't be combined"); + } on StateError catch (err) { + print("$from and $to can't be combined"); + print(err); } } } diff --git a/lib/untis/lesson.dart b/lib/untis/lesson.dart index 9513bd8..64145df 100644 --- a/lib/untis/lesson.dart +++ b/lib/untis/lesson.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../util/serializable.dart'; @@ -21,6 +22,12 @@ class Lesson { int get startMinutes => start.inMinutes - UntisLesson.startOfDay.inMinutes; int get endMinutes => end.inMinutes - UntisLesson.startOfDay.inMinutes; + String get description => + 'Lesson (${lecture.name} $start $end $rooms $teachers)'; + + @override + String toString() => description; + Lesson({ required this.lecture, required this.rooms, @@ -34,25 +41,40 @@ class Lesson { if (a.lecture != b.lecture) { throw StateError("Lectures of combined lessons don't match"); } - if (a.rooms.difference(b.rooms).isNotEmpty) { - throw StateError("Rooms of combined lessons don't match"); - } - if (a.teachers.difference(b.teachers).isNotEmpty) { - throw StateError("Teachers of combined lessons don't match"); - } if (a.date != b.date) { throw StateError("Dates of combined lessons don't match"); } + final doRoomsOverlap = a.rooms.intersection(b.rooms).isNotEmpty; + final areTeachersEqual = setEquals(a.teachers, b.teachers); + if (!doRoomsOverlap && !areTeachersEqual) { + throw StateError( + 'Neither rooms nor teachers of combined lessons overlap'); + } return Lesson( lecture: a.lecture, - rooms: a.rooms, - teachers: a.teachers, + rooms: {...a.rooms, ...b.rooms}, + teachers: {...a.teachers, ...b.teachers}, date: a.date, start: a.startMinutes < b.startMinutes ? a.start : b.start, end: a.endMinutes > b.endMinutes ? a.end : b.end, ); } + + @override + int get hashCode => Object.hash(lecture, rooms, teachers, date, start, end); + + @override + bool operator ==(Object other) { + if (other is! Lesson) return false; + + return other.lecture == lecture && + setEquals(other.rooms, rooms) && + setEquals(other.teachers, teachers) && + other.date == date && + other.start == start && + other.end == end; + } } class UntisLesson extends Lesson with UntisMixin { @@ -63,7 +85,7 @@ class UntisLesson extends Lesson with UntisMixin { final Json json; @override - String get name => 'Lesson (${lecture.name})'; + String get name => description; UntisLesson( this.json, { diff --git a/test/lesson_merge_test.dart b/test/lesson_merge_test.dart new file mode 100644 index 0000000..abf0b30 --- /dev/null +++ b/test/lesson_merge_test.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:ontime/untis/lecture.dart'; +import 'package:ontime/untis/lesson.dart'; +import 'package:ontime/untis/room.dart'; +import 'package:ontime/untis/teacher.dart'; + +void main() { + final lectureA = MockLecture(id: 0, name: 'A'); + // final lectureB = MockLecture(id: 1, name: 'B'); + + final roomA = MockRoom(id: 2, name: 'A'); + final roomB = MockRoom(id: 3, name: 'B'); + + final teacherA = MockTeacher(id: 4, name: 'A'); + final teacherB = MockTeacher(id: 5, name: 'B'); + + final date = DateTime(2025, 1, 1); + + const timeLesson1 = TimeOfDay(hour: 8, minute: 0); + const timeLesson2 = TimeOfDay(hour: 9, minute: 30); + const timeLesson3 = TimeOfDay(hour: 11, minute: 0); + + test('Sequential lessons with no differences', () { + expect( + Lesson.merge( + Lesson( + lecture: lectureA, + rooms: {roomA}, + teachers: {teacherA}, + date: date, + start: timeLesson1, + end: timeLesson2, + ), + Lesson( + lecture: lectureA, + rooms: {roomA}, + teachers: {teacherA}, + date: date, + start: timeLesson2, + end: timeLesson3, + )), + Lesson( + lecture: lectureA, + rooms: {roomA}, + teachers: {teacherA}, + date: date, + start: timeLesson1, + end: timeLesson3, + )); + }); + + test('Simultaneous lessons with same teacher, different rooms', () { + expect( + Lesson.merge( + Lesson( + lecture: lectureA, + rooms: {roomA}, + teachers: {teacherA}, + date: date, + start: timeLesson1, + end: timeLesson2, + ), + Lesson( + lecture: lectureA, + rooms: {roomB}, + teachers: {teacherA}, + date: date, + start: timeLesson1, + end: timeLesson2, + )), + Lesson( + lecture: lectureA, + rooms: {roomA, roomB}, + teachers: {teacherA}, + date: date, + start: timeLesson1, + end: timeLesson2, + )); + }); + + test('Simultaneous lessons with same room, different teachers', () { + expect( + Lesson.merge( + Lesson( + lecture: lectureA, + rooms: {roomA}, + teachers: {teacherA}, + date: date, + start: timeLesson1, + end: timeLesson2, + ), + Lesson( + lecture: lectureA, + rooms: {roomA}, + teachers: {teacherB}, + date: date, + start: timeLesson1, + end: timeLesson2, + )), + Lesson( + lecture: lectureA, + rooms: {roomA}, + teachers: {teacherA, teacherB}, + date: date, + start: timeLesson1, + end: timeLesson2, + )); + }); + + test('Simultaneous lessons with overlapping rooms', () { + expect( + Lesson.merge( + Lesson( + lecture: lectureA, + rooms: {roomA}, + teachers: {teacherA}, + date: date, + start: timeLesson1, + end: timeLesson2, + ), + Lesson( + lecture: lectureA, + rooms: {roomA, roomB}, + teachers: {teacherB}, + date: date, + start: timeLesson1, + end: timeLesson2, + )), + Lesson( + lecture: lectureA, + rooms: {roomA, roomB}, + teachers: {teacherA, teacherB}, + date: date, + start: timeLesson1, + end: timeLesson2, + )); + }); +} + +class MockLecture extends Lecture { + MockLecture({required int id, required String name}) + : super({'id': id, 'name': name, 'longName': name}); +} + +class MockRoom extends Room { + MockRoom({required int id, required String name}) + : super({'id': id, 'name': name}); +} + +class MockTeacher extends Teacher { + MockTeacher({required int id, required String name}) + : super({'id': id, 'name': name, 'longName': name}); +} -- GitLab