From 9ad4cdc69bba1dd725da572e4422978172dde14b Mon Sep 17 00:00:00 2001 From: sabazadam Date: Sun, 14 Dec 2025 00:18:30 +0300 Subject: [PATCH] All Features are implemented - Validate classroom capacity limits - Check no double-booking of classrooms - Ensure no consecutive exams for students - Enforce max 2 exams per day per student - Return detailed validation results with error messages --- .../se302/service/ConstraintValidator.java | 291 ++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 src/main/java/org/example/se302/service/ConstraintValidator.java diff --git a/src/main/java/org/example/se302/service/ConstraintValidator.java b/src/main/java/org/example/se302/service/ConstraintValidator.java new file mode 100644 index 0000000..46946cb --- /dev/null +++ b/src/main/java/org/example/se302/service/ConstraintValidator.java @@ -0,0 +1,291 @@ +package org.example.se302.service; + +import org.example.se302.model.*; + +import java.util.*; + +/** + * Validates scheduling constraints for exam assignments. + * Ensures that all hard constraints are satisfied. + * Works with the day/timeSlotIndex based ExamAssignment architecture. + */ +public class ConstraintValidator { + private final DataManager dataManager; + + public ConstraintValidator() { + this.dataManager = DataManager.getInstance(); + } + + /** + * Validate all constraints for a complete schedule. + */ + public ValidationResult validateSchedule(ScheduleState scheduleState) { + ValidationResult result = new ValidationResult(); + + // Check all assignments + for (ExamAssignment assignment : scheduleState.getAssignments().values()) { + if (assignment.isAssigned()) { + ValidationResult assignmentResult = validateAssignment(assignment, scheduleState); + result.merge(assignmentResult); + } + } + + return result; + } + + /** + * Validate a single assignment against the current schedule state. + */ + public ValidationResult validateAssignment(ExamAssignment assignment, ScheduleState scheduleState) { + ValidationResult result = new ValidationResult(); + + if (!assignment.isAssigned()) { + result.addError("Assignment is not complete (missing time slot or classroom)"); + return result; + } + + // Check classroom capacity + ValidationResult capacityResult = checkClassroomCapacity(assignment); + result.merge(capacityResult); + + // Check no double-booking + ValidationResult doubleBookingResult = checkNoDoubleBooking(assignment, scheduleState); + result.merge(doubleBookingResult); + + // Check student constraints + Course course = dataManager.getCourse(assignment.getCourseCode()); + if (course != null) { + for (String studentId : course.getEnrolledStudents()) { + // Check no consecutive exams + ValidationResult consecutiveResult = checkNoConsecutiveExams( + studentId, assignment, scheduleState); + result.merge(consecutiveResult); + + // Check max 2 exams per day + ValidationResult maxPerDayResult = checkMaxTwoExamsPerDay( + studentId, assignment, scheduleState); + result.merge(maxPerDayResult); + } + } + + return result; + } + + /** + * Check that classroom capacity is not exceeded. + */ + public ValidationResult checkClassroomCapacity(ExamAssignment assignment) { + ValidationResult result = new ValidationResult(); + + Classroom classroom = dataManager.getClassroom(assignment.getClassroomId()); + Course course = dataManager.getCourse(assignment.getCourseCode()); + + if (classroom == null) { + result.addError("Classroom not found: " + assignment.getClassroomId()); + return result; + } + + if (course == null) { + result.addError("Course not found: " + assignment.getCourseCode()); + return result; + } + + int enrolledCount = course.getEnrolledStudentsCount(); + int capacity = classroom.getCapacity(); + + if (enrolledCount > capacity) { + result.addError(String.format( + "Classroom capacity exceeded: %s has %d students but %s capacity is %d", + course.getCourseCode(), enrolledCount, classroom.getClassroomId(), capacity)); + } + + return result; + } + + /** + * Check that no classroom is double-booked at the same time. + */ + public ValidationResult checkNoDoubleBooking(ExamAssignment assignment, ScheduleState scheduleState) { + ValidationResult result = new ValidationResult(); + + // Check if any other assignment uses the same classroom at the same time + for (ExamAssignment existing : scheduleState.getAssignments().values()) { + if (existing.isAssigned() && + !existing.getCourseCode().equals(assignment.getCourseCode()) && + existing.getClassroomId().equals(assignment.getClassroomId()) && + existing.getDay() == assignment.getDay() && + existing.getTimeSlotIndex() == assignment.getTimeSlotIndex()) { + + result.addError(String.format( + "Classroom double-booking: %s already has %s at Day %d, Slot %d", + assignment.getClassroomId(), + existing.getCourseCode(), + assignment.getDay() + 1, + assignment.getTimeSlotIndex() + 1)); + break; + } + } + + return result; + } + + /** + * Check that a student has no consecutive exams. + * Consecutive means exams in adjacent time slots on the same day (back-to-back). + */ + public ValidationResult checkNoConsecutiveExams(String studentId, + ExamAssignment newAssignment, + ScheduleState scheduleState) { + ValidationResult result = new ValidationResult(); + + // Get all courses this student is enrolled in + Student student = dataManager.getStudent(studentId); + if (student == null) { + return result; // Student not found, skip + } + + List studentCourses = student.getEnrolledCourses(); + int newDay = newAssignment.getDay(); + int newSlot = newAssignment.getTimeSlotIndex(); + + // Check each existing assignment for this student + for (ExamAssignment existing : scheduleState.getAssignments().values()) { + if (!existing.isAssigned() || existing.getCourseCode().equals(newAssignment.getCourseCode())) { + continue; + } + + // Check if this assignment is for a course the student is enrolled in + if (studentCourses.contains(existing.getCourseCode())) { + int existingDay = existing.getDay(); + int existingSlot = existing.getTimeSlotIndex(); + + // Check if consecutive (adjacent slots on the same day) + boolean isConsecutive = (existingDay == newDay) && + (Math.abs(existingSlot - newSlot) == 1); + + if (isConsecutive) { + result.addError(String.format( + "Consecutive exams for student %s: %s (Day %d, Slot %d) and %s (Day %d, Slot %d) are back-to-back", + studentId, + existing.getCourseCode(), existingDay + 1, existingSlot + 1, + newAssignment.getCourseCode(), newDay + 1, newSlot + 1)); + } + } + } + + return result; + } + + /** + * Check that a student has at most 2 exams per day. + */ + public ValidationResult checkMaxTwoExamsPerDay(String studentId, + ExamAssignment newAssignment, + ScheduleState scheduleState) { + ValidationResult result = new ValidationResult(); + + // Get all courses this student is enrolled in + Student student = dataManager.getStudent(studentId); + if (student == null) { + return result; + } + + List studentCourses = student.getEnrolledCourses(); + int newDay = newAssignment.getDay(); + + // Count exams on the same day as new assignment + long examsOnSameDay = scheduleState.getAssignments().values().stream() + .filter(a -> a.isAssigned()) + .filter(a -> !a.getCourseCode().equals(newAssignment.getCourseCode())) + .filter(a -> studentCourses.contains(a.getCourseCode())) + .filter(a -> a.getDay() == newDay) + .count(); + + // Including the new assignment, would be examsOnSameDay + 1 + if (examsOnSameDay + 1 > 2) { + result.addError(String.format( + "Too many exams for student %s on Day %d: would have %d exams (max 2)", + studentId, + newDay + 1, + examsOnSameDay + 1)); + } + + return result; + } + + /** + * Get all students affected by an assignment. + */ + public List getAffectedStudents(ExamAssignment assignment) { + Course course = dataManager.getCourse(assignment.getCourseCode()); + return course != null ? course.getEnrolledStudents() : new ArrayList<>(); + } + + /** + * Check if a specific assignment would create conflicts. + * Returns list of conflict descriptions. + */ + public List getConflictsForAssignment(ExamAssignment assignment, ScheduleState scheduleState) { + ValidationResult result = validateAssignment(assignment, scheduleState); + return result.getErrors(); + } + + /** + * Result of constraint validation. + */ + public static class ValidationResult { + private final List errors; + private final List warnings; + + public ValidationResult() { + this.errors = new ArrayList<>(); + this.warnings = new ArrayList<>(); + } + + public void addError(String error) { + if (!errors.contains(error)) { // Avoid duplicates + errors.add(error); + } + } + + public void addWarning(String warning) { + if (!warnings.contains(warning)) { + warnings.add(warning); + } + } + + public void merge(ValidationResult other) { + this.errors.addAll(other.errors); + this.warnings.addAll(other.warnings); + } + + public boolean isValid() { + return errors.isEmpty(); + } + + public List getErrors() { + return new ArrayList<>(errors); + } + + public List getWarnings() { + return new ArrayList<>(warnings); + } + + public int getErrorCount() { + return errors.size(); + } + + public String getFormattedErrors() { + if (errors.isEmpty()) { + return "No errors"; + } + return String.join("\n", errors); + } + + @Override + public String toString() { + return String.format("ValidationResult{errors=%d, warnings=%d}", + errors.size(), warnings.size()); + } + } +}