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