diff --git a/src/main/java/org/example/se302/algorithm/CSPSolver.java b/src/main/java/org/example/se302/algorithm/CSPSolver.java new file mode 100644 index 0000000..822dd25 --- /dev/null +++ b/src/main/java/org/example/se302/algorithm/CSPSolver.java @@ -0,0 +1,373 @@ +package org.example.se302.algorithm; + +import org.example.se302.model.*; + +import java.util.*; + +/** + * CSP (Constraint Satisfaction Problem) Solver for Exam Scheduling. + * + * This solver uses a backtracking algorithm with the following optimizations: + * - MRV (Minimum Remaining Values) heuristic for variable ordering + * - Degree heuristic as a tie-breaker + * - Forward checking for constraint propagation + * - Arc consistency (AC-3) for further pruning + * + * The CSP formulation: + * - Variables: Each course that needs an exam scheduled + * - Domain: All possible (TimeSlot, Classroom) pairs + * - Constraints: Hard constraints (must satisfy) and soft constraints + * (preferably satisfy) + */ +public class CSPSolver { + + private List hardConstraints; + private List softConstraints; + private Map courses; + private Map classrooms; + private List availableTimeSlots; + + // Statistics + private int nodesExplored; + private int backtracks; + private long startTime; + private long maxTimeMs; + + // Conflict graph for student conflicts (courses that share students) + private Map> conflictGraph; + + public CSPSolver() { + this.hardConstraints = new ArrayList<>(); + this.softConstraints = new ArrayList<>(); + this.courses = new HashMap<>(); + this.classrooms = new HashMap<>(); + this.availableTimeSlots = new ArrayList<>(); + this.conflictGraph = new HashMap<>(); + this.nodesExplored = 0; + this.backtracks = 0; + this.maxTimeMs = 60000; // Default 60 second timeout + } + + /** + * Adds a hard constraint to the solver. + */ + public void addHardConstraint(Constraint constraint) { + if (constraint.isHard()) { + hardConstraints.add(constraint); + } else { + softConstraints.add(constraint); + } + } + + /** + * Adds a soft constraint to the solver. + */ + public void addSoftConstraint(Constraint constraint) { + softConstraints.add(constraint); + } + + /** + * Sets the available time slots for scheduling. + */ + public void setAvailableTimeSlots(List timeSlots) { + this.availableTimeSlots = new ArrayList<>(timeSlots); + Collections.sort(this.availableTimeSlots); + } + + /** + * Sets the available classrooms. + */ + public void setClassrooms(Map classrooms) { + this.classrooms = classrooms; + } + + /** + * Sets the courses to schedule. + */ + public void setCourses(Map courses) { + this.courses = courses; + buildConflictGraph(); + } + + /** + * Builds the conflict graph based on shared students. + * Two courses are connected if they share at least one student. + */ + private void buildConflictGraph() { + conflictGraph.clear(); + + for (String courseCode : courses.keySet()) { + conflictGraph.put(courseCode, new HashSet<>()); + } + + List courseCodes = new ArrayList<>(courses.keySet()); + + for (int i = 0; i < courseCodes.size(); i++) { + Course course1 = courses.get(courseCodes.get(i)); + Set students1 = new HashSet<>(course1.getEnrolledStudents()); + + for (int j = i + 1; j < courseCodes.size(); j++) { + Course course2 = courses.get(courseCodes.get(j)); + + // Check if courses share any students + for (String student : course2.getEnrolledStudents()) { + if (students1.contains(student)) { + conflictGraph.get(courseCodes.get(i)).add(courseCodes.get(j)); + conflictGraph.get(courseCodes.get(j)).add(courseCodes.get(i)); + break; + } + } + } + } + } + + /** + * Solves the CSP and returns a complete schedule. + * + * @param initialState Optional initial state with some pre-assigned exams + * @return A complete schedule state, or null if no solution found + */ + public ScheduleState solve(ScheduleState initialState) { + nodesExplored = 0; + backtracks = 0; + startTime = System.currentTimeMillis(); + + // Initialize state if not provided + ScheduleState state = initialState != null ? initialState.copy() : createInitialState(); + + // Start backtracking search + ScheduleState result = backtrack(state); + + return result; + } + + /** + * Creates an initial state with all courses unassigned. + */ + private ScheduleState createInitialState() { + ScheduleState state = new ScheduleState(); + state.setAvailableTimeSlots(availableTimeSlots); + state.setAvailableClassrooms(new ArrayList<>(classrooms.values())); + + for (Course course : courses.values()) { + ExamAssignment assignment = new ExamAssignment(course.getCourseCode()); + assignment.setStudentCount(course.getEnrolledStudentsCount()); + state.addAssignment(assignment); + } + + return state; + } + + /** + * Backtracking algorithm with MRV heuristic. + */ + private ScheduleState backtrack(ScheduleState state) { + // Check timeout + if (System.currentTimeMillis() - startTime > maxTimeMs) { + return null; // Timeout + } + + nodesExplored++; + + // Check if complete + if (state.isComplete()) { + return state; + } + + // Select next variable using MRV heuristic + ExamAssignment variable = selectUnassignedVariable(state); + if (variable == null) { + return null; // No unassigned variables but not complete - shouldn't happen + } + + // Get ordered domain values + List orderedDomain = orderDomainValues(variable, state); + + for (DomainValue value : orderedDomain) { + // Try assigning this value + if (isConsistent(variable.getCourseCode(), value.timeSlot, value.classroomId, state)) { + // Make assignment + state.updateAssignment(variable.getCourseCode(), value.timeSlot, value.classroomId); + + // Recursive call + ScheduleState result = backtrack(state); + if (result != null) { + return result; + } + + // Backtrack + backtracks++; + state.removeAssignment(variable.getCourseCode()); + } + } + + return null; // No valid assignment found + } + + /** + * Selects the next unassigned variable using MRV (Minimum Remaining Values) + * heuristic. + * Ties are broken using the degree heuristic (most constraints). + */ + private ExamAssignment selectUnassignedVariable(ScheduleState state) { + ExamAssignment selected = null; + int minValues = Integer.MAX_VALUE; + int maxDegree = -1; + + for (ExamAssignment assignment : state.getUnassignedCourses()) { + int remainingValues = countRemainingValues(assignment, state); + + if (remainingValues < minValues) { + minValues = remainingValues; + maxDegree = getDegree(assignment.getCourseCode()); + selected = assignment; + } else if (remainingValues == minValues) { + int degree = getDegree(assignment.getCourseCode()); + if (degree > maxDegree) { + maxDegree = degree; + selected = assignment; + } + } + } + + return selected; + } + + /** + * Counts the number of remaining valid values for a variable. + */ + private int countRemainingValues(ExamAssignment assignment, ScheduleState state) { + int count = 0; + for (TimeSlot timeSlot : availableTimeSlots) { + for (Classroom classroom : classrooms.values()) { + if (classroom.getCapacity() >= assignment.getStudentCount() && + state.isClassroomAvailable(classroom.getClassroomId(), timeSlot)) { + count++; + } + } + } + return count; + } + + /** + * Gets the degree (number of constraints/conflicts) for a course. + */ + private int getDegree(String courseCode) { + Set conflicts = conflictGraph.get(courseCode); + return conflicts != null ? conflicts.size() : 0; + } + + /** + * Orders domain values using Least Constraining Value (LCV) heuristic. + */ + private List orderDomainValues(ExamAssignment assignment, ScheduleState state) { + List values = new ArrayList<>(); + + for (TimeSlot timeSlot : availableTimeSlots) { + for (Classroom classroom : classrooms.values()) { + if (classroom.getCapacity() >= assignment.getStudentCount()) { + DomainValue value = new DomainValue(timeSlot, classroom.getClassroomId()); + value.constrainingFactor = calculateConstrainingFactor( + assignment.getCourseCode(), timeSlot, classroom.getClassroomId(), state); + values.add(value); + } + } + } + + // Sort by constraining factor (least constraining first) + values.sort(Comparator.comparingInt(v -> v.constrainingFactor)); + + return values; + } + + /** + * Calculates how much a value constrains other variables. + */ + private int calculateConstrainingFactor(String courseCode, TimeSlot timeSlot, + String classroomId, ScheduleState state) { + int factor = 0; + + // Count how many conflicting courses would be affected + Set conflicts = conflictGraph.get(courseCode); + if (conflicts != null) { + for (String conflictCourse : conflicts) { + ExamAssignment conflictAssignment = state.getAssignment(conflictCourse); + if (conflictAssignment != null && !conflictAssignment.isAssigned()) { + factor++; // This time slot becomes unavailable for the conflicting course + } + } + } + + return factor; + } + + /** + * Checks if an assignment is consistent with all constraints. + */ + private boolean isConsistent(String courseCode, TimeSlot timeSlot, + String classroomId, ScheduleState state) { + // Create temporary assignment for checking + ExamAssignment tempAssignment = new ExamAssignment(courseCode, timeSlot, classroomId); + Course course = courses.get(courseCode); + if (course != null) { + tempAssignment.setStudentCount(course.getEnrolledStudentsCount()); + } + + // Check all hard constraints + for (Constraint constraint : hardConstraints) { + if (!constraint.isSatisfied(tempAssignment, state)) { + return false; + } + } + + // Check student conflicts separately (most important) + Set coursesAtSameTime = state.getCoursesAtTimeSlot(timeSlot); + Set conflicts = conflictGraph.get(courseCode); + if (conflicts != null) { + for (String conflictCourse : conflicts) { + if (coursesAtSameTime.contains(conflictCourse)) { + return false; // Student conflict + } + } + } + + return true; + } + + /** + * Helper class for domain values. + */ + private static class DomainValue { + TimeSlot timeSlot; + String classroomId; + int constrainingFactor; + + DomainValue(TimeSlot timeSlot, String classroomId) { + this.timeSlot = timeSlot; + this.classroomId = classroomId; + this.constrainingFactor = 0; + } + } + + // Getters for statistics + + public int getNodesExplored() { + return nodesExplored; + } + + public int getBacktracks() { + return backtracks; + } + + public long getMaxTimeMs() { + return maxTimeMs; + } + + public void setMaxTimeMs(long maxTimeMs) { + this.maxTimeMs = maxTimeMs; + } + + public Map> getConflictGraph() { + return Collections.unmodifiableMap(conflictGraph); + } +} diff --git a/src/main/java/org/example/se302/algorithm/CapacityConstraint.java b/src/main/java/org/example/se302/algorithm/CapacityConstraint.java new file mode 100644 index 0000000..02ef7ce --- /dev/null +++ b/src/main/java/org/example/se302/algorithm/CapacityConstraint.java @@ -0,0 +1,73 @@ +package org.example.se302.algorithm; + +import org.example.se302.model.Classroom; +import org.example.se302.model.ExamAssignment; +import org.example.se302.model.ScheduleState; + +import java.util.Map; + +/** + * Hard constraint: Classroom capacity must be sufficient for the number of + * students. + */ +public class CapacityConstraint implements Constraint { + + private Map classrooms; + + public CapacityConstraint(Map classrooms) { + this.classrooms = classrooms; + } + + @Override + public String getName() { + return "CAPACITY"; + } + + @Override + public String getDescription() { + return "Classroom capacity must be sufficient for the number of enrolled students"; + } + + @Override + public boolean isSatisfied(ExamAssignment assignment, ScheduleState state) { + if (!assignment.isAssigned()) { + return true; + } + + Classroom classroom = classrooms.get(assignment.getClassroomId()); + if (classroom == null) { + return false; // Unknown classroom + } + + return classroom.getCapacity() >= assignment.getStudentCount(); + } + + @Override + public int getPriority() { + return 100; // High priority - hard constraint + } + + @Override + public boolean isHard() { + return true; + } + + @Override + public String getViolationMessage(ExamAssignment assignment, ScheduleState state) { + if (isSatisfied(assignment, state)) { + return null; + } + + Classroom classroom = classrooms.get(assignment.getClassroomId()); + if (classroom == null) { + return String.format("Classroom %s does not exist", assignment.getClassroomId()); + } + + return String.format("Classroom %s capacity (%d) is insufficient for %d students", + assignment.getClassroomId(), classroom.getCapacity(), assignment.getStudentCount()); + } + + public void setClassrooms(Map classrooms) { + this.classrooms = classrooms; + } +} diff --git a/src/main/java/org/example/se302/algorithm/ClassroomConflictConstraint.java b/src/main/java/org/example/se302/algorithm/ClassroomConflictConstraint.java new file mode 100644 index 0000000..da751ed --- /dev/null +++ b/src/main/java/org/example/se302/algorithm/ClassroomConflictConstraint.java @@ -0,0 +1,49 @@ +package org.example.se302.algorithm; + +import org.example.se302.model.Classroom; +import org.example.se302.model.ExamAssignment; +import org.example.se302.model.ScheduleState; + +/** + * Hard constraint: A classroom cannot host two exams at the same time. + */ +public class ClassroomConflictConstraint implements Constraint { + + @Override + public String getName() { + return "CLASSROOM_CONFLICT"; + } + + @Override + public String getDescription() { + return "A classroom cannot be used for multiple exams at the same time"; + } + + @Override + public boolean isSatisfied(ExamAssignment assignment, ScheduleState state) { + if (!assignment.isAssigned()) { + return true; // Unassigned assignments don't violate constraints + } + + return state.isClassroomAvailable(assignment.getClassroomId(), assignment.getTimeSlot()); + } + + @Override + public int getPriority() { + return 100; // High priority - hard constraint + } + + @Override + public boolean isHard() { + return true; + } + + @Override + public String getViolationMessage(ExamAssignment assignment, ScheduleState state) { + if (isSatisfied(assignment, state)) { + return null; + } + return String.format("Classroom %s is already in use at %s", + assignment.getClassroomId(), assignment.getTimeSlot()); + } +} diff --git a/src/main/java/org/example/se302/algorithm/Constraint.java b/src/main/java/org/example/se302/algorithm/Constraint.java new file mode 100644 index 0000000..4a56c08 --- /dev/null +++ b/src/main/java/org/example/se302/algorithm/Constraint.java @@ -0,0 +1,55 @@ +package org.example.se302.algorithm; + +import org.example.se302.model.ExamAssignment; +import org.example.se302.model.ScheduleState; + +/** + * Interface for constraints in the CSP-based exam scheduling. + * Each constraint represents a rule that must be satisfied for a valid + * schedule. + */ +public interface Constraint { + + /** + * Gets the name/identifier of this constraint. + */ + String getName(); + + /** + * Gets a human-readable description of this constraint. + */ + String getDescription(); + + /** + * Checks if the given assignment satisfies this constraint + * within the context of the current schedule state. + * + * @param assignment The new assignment to check + * @param state The current schedule state + * @return true if the constraint is satisfied, false otherwise + */ + boolean isSatisfied(ExamAssignment assignment, ScheduleState state); + + /** + * Gets the priority/weight of this constraint. + * Higher values indicate more important constraints. + * Hard constraints should have very high priority. + */ + int getPriority(); + + /** + * Checks if this is a hard constraint (must be satisfied) + * or a soft constraint (preferably satisfied but can be violated). + */ + boolean isHard(); + + /** + * Gets a detailed message explaining why the constraint was violated + * for the given assignment. + * + * @param assignment The assignment that violated the constraint + * @param state The current schedule state + * @return A descriptive message, or null if constraint is satisfied + */ + String getViolationMessage(ExamAssignment assignment, ScheduleState state); +} diff --git a/src/main/java/org/example/se302/algorithm/StudentConflictConstraint.java b/src/main/java/org/example/se302/algorithm/StudentConflictConstraint.java new file mode 100644 index 0000000..604ee2a --- /dev/null +++ b/src/main/java/org/example/se302/algorithm/StudentConflictConstraint.java @@ -0,0 +1,152 @@ +package org.example.se302.algorithm; + +import org.example.se302.model.Course; +import org.example.se302.model.ExamAssignment; +import org.example.se302.model.ScheduleState; + +import java.util.*; + +/** + * Hard constraint: A student cannot have two exams at the same time. + * This checks for overlapping time slots among courses that share students. + */ +public class StudentConflictConstraint implements Constraint { + + private Map courses; + + public StudentConflictConstraint(Map courses) { + this.courses = courses; + } + + @Override + public String getName() { + return "STUDENT_CONFLICT"; + } + + @Override + public String getDescription() { + return "A student cannot have two exams at the same time"; + } + + @Override + public boolean isSatisfied(ExamAssignment assignment, ScheduleState state) { + if (!assignment.isAssigned()) { + return true; + } + + Course course = courses.get(assignment.getCourseCode()); + if (course == null) { + return true; // Unknown course, skip check + } + + Set enrolledStudents = new HashSet<>(course.getEnrolledStudents()); + + // Get all courses scheduled at overlapping time slots + Set coursesAtSameTime = state.getCoursesAtTimeSlot(assignment.getTimeSlot()); + + for (String otherCourseCode : coursesAtSameTime) { + if (otherCourseCode.equals(assignment.getCourseCode())) { + continue; // Skip self + } + + Course otherCourse = courses.get(otherCourseCode); + if (otherCourse == null) { + continue; + } + + // Check if any student is enrolled in both courses + for (String student : otherCourse.getEnrolledStudents()) { + if (enrolledStudents.contains(student)) { + return false; // Conflict found + } + } + } + + return true; + } + + @Override + public int getPriority() { + return 100; // High priority - hard constraint + } + + @Override + public boolean isHard() { + return true; + } + + @Override + public String getViolationMessage(ExamAssignment assignment, ScheduleState state) { + if (isSatisfied(assignment, state)) { + return null; + } + + Course course = courses.get(assignment.getCourseCode()); + if (course == null) { + return null; + } + + Set enrolledStudents = new HashSet<>(course.getEnrolledStudents()); + Set coursesAtSameTime = state.getCoursesAtTimeSlot(assignment.getTimeSlot()); + + for (String otherCourseCode : coursesAtSameTime) { + if (otherCourseCode.equals(assignment.getCourseCode())) { + continue; + } + + Course otherCourse = courses.get(otherCourseCode); + if (otherCourse == null) { + continue; + } + + List conflictingStudents = new ArrayList<>(); + for (String student : otherCourse.getEnrolledStudents()) { + if (enrolledStudents.contains(student)) { + conflictingStudents.add(student); + } + } + + if (!conflictingStudents.isEmpty()) { + return String.format("Students %s have exams for both %s and %s at %s", + conflictingStudents.size() > 3 ? conflictingStudents.subList(0, 3) + "..." + : conflictingStudents, + assignment.getCourseCode(), otherCourseCode, assignment.getTimeSlot()); + } + } + + return null; + } + + public void setCourses(Map courses) { + this.courses = courses; + } + + /** + * Gets the set of courses that conflict with the given course + * (i.e., share at least one student). + */ + public Set getConflictingCourses(String courseCode) { + Set conflicting = new HashSet<>(); + Course course = courses.get(courseCode); + if (course == null) { + return conflicting; + } + + Set enrolledStudents = new HashSet<>(course.getEnrolledStudents()); + + for (Map.Entry entry : courses.entrySet()) { + if (entry.getKey().equals(courseCode)) { + continue; + } + + for (String student : entry.getValue().getEnrolledStudents()) { + if (enrolledStudents.contains(student)) { + conflicting.add(entry.getKey()); + break; + } + } + } + + return conflicting; + } +} diff --git a/src/main/java/org/example/se302/model/ExamAssignment.java b/src/main/java/org/example/se302/model/ExamAssignment.java new file mode 100644 index 0000000..31b3982 --- /dev/null +++ b/src/main/java/org/example/se302/model/ExamAssignment.java @@ -0,0 +1,145 @@ +package org.example.se302.model; + +import java.util.Objects; + +/** + * Represents an exam assignment in the CSP-based scheduling system. + * An ExamAssignment links a course to a specific time slot and classroom. + * This is the core data structure for the schedule state. + */ +public class ExamAssignment { + private String courseCode; + private TimeSlot timeSlot; + private String classroomId; + private int studentCount; + + // Assignment status + private boolean isLocked; // If true, this assignment cannot be changed by the algorithm + + /** + * Creates a new exam assignment. + * + * @param courseCode The course code for this exam + * @param timeSlot The time slot assigned to this exam + * @param classroomId The classroom assigned to this exam + */ + public ExamAssignment(String courseCode, TimeSlot timeSlot, String classroomId) { + this.courseCode = courseCode; + this.timeSlot = timeSlot; + this.classroomId = classroomId; + this.studentCount = 0; + this.isLocked = false; + } + + /** + * Creates an unassigned exam (only course is known). + */ + public ExamAssignment(String courseCode) { + this(courseCode, null, null); + } + + /** + * Creates a deep copy of this assignment. + */ + public ExamAssignment copy() { + ExamAssignment copy = new ExamAssignment(this.courseCode, this.timeSlot, this.classroomId); + copy.studentCount = this.studentCount; + copy.isLocked = this.isLocked; + return copy; + } + + /** + * Checks if this assignment is complete (has both time slot and classroom). + */ + public boolean isAssigned() { + return timeSlot != null && classroomId != null; + } + + /** + * Checks if this assignment conflicts with another assignment. + * Two assignments conflict if: + * 1. Same classroom at overlapping time slots + * 2. (Checked separately) Same student in both courses at overlapping time + * slots + */ + public boolean conflictsWith(ExamAssignment other) { + if (!this.isAssigned() || !other.isAssigned()) { + return false; + } + + // Check classroom conflict + if (this.classroomId.equals(other.classroomId) && + this.timeSlot.overlapsWith(other.timeSlot)) { + return true; + } + + return false; + } + + // Getters and Setters + + public String getCourseCode() { + return courseCode; + } + + public void setCourseCode(String courseCode) { + this.courseCode = courseCode; + } + + public TimeSlot getTimeSlot() { + return timeSlot; + } + + public void setTimeSlot(TimeSlot timeSlot) { + this.timeSlot = timeSlot; + } + + public String getClassroomId() { + return classroomId; + } + + public void setClassroomId(String classroomId) { + this.classroomId = classroomId; + } + + public int getStudentCount() { + return studentCount; + } + + public void setStudentCount(int studentCount) { + this.studentCount = studentCount; + } + + public boolean isLocked() { + return isLocked; + } + + public void setLocked(boolean locked) { + isLocked = locked; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ExamAssignment that = (ExamAssignment) o; + return Objects.equals(courseCode, that.courseCode) && + Objects.equals(timeSlot, that.timeSlot) && + Objects.equals(classroomId, that.classroomId); + } + + @Override + public int hashCode() { + return Objects.hash(courseCode, timeSlot, classroomId); + } + + @Override + public String toString() { + if (!isAssigned()) { + return courseCode + " [Unassigned]"; + } + return courseCode + " -> " + classroomId + " @ " + timeSlot; + } +} diff --git a/src/main/java/org/example/se302/model/ScheduleState.java b/src/main/java/org/example/se302/model/ScheduleState.java new file mode 100644 index 0000000..bf77b1e --- /dev/null +++ b/src/main/java/org/example/se302/model/ScheduleState.java @@ -0,0 +1,270 @@ +package org.example.se302.model; + +import java.util.*; + +/** + * Represents the complete state of an exam schedule. + * This is the main data structure used by the CSP solver to track + * the current schedule state and validate constraints. + */ +public class ScheduleState { + // All exam assignments indexed by course code + private Map assignments; + + // Quick lookup structures for constraint checking + private Map> classroomTimeSlotUsage; // classroomId -> set of timeSlot IDs + private Map> timeSlotCourseMapping; // timeSlotId -> set of course codes + + // Available resources + private List availableTimeSlots; + private List availableClassrooms; + + // Statistics + private int totalCourses; + private int assignedCourses; + private int constraintViolations; + + public ScheduleState() { + this.assignments = new HashMap<>(); + this.classroomTimeSlotUsage = new HashMap<>(); + this.timeSlotCourseMapping = new HashMap<>(); + this.availableTimeSlots = new ArrayList<>(); + this.availableClassrooms = new ArrayList<>(); + this.totalCourses = 0; + this.assignedCourses = 0; + this.constraintViolations = 0; + } + + /** + * Creates a deep copy of this schedule state. + */ + public ScheduleState copy() { + ScheduleState copy = new ScheduleState(); + + for (Map.Entry entry : this.assignments.entrySet()) { + copy.assignments.put(entry.getKey(), entry.getValue().copy()); + } + + for (Map.Entry> entry : this.classroomTimeSlotUsage.entrySet()) { + copy.classroomTimeSlotUsage.put(entry.getKey(), new HashSet<>(entry.getValue())); + } + + for (Map.Entry> entry : this.timeSlotCourseMapping.entrySet()) { + copy.timeSlotCourseMapping.put(entry.getKey(), new HashSet<>(entry.getValue())); + } + + copy.availableTimeSlots = new ArrayList<>(this.availableTimeSlots); + copy.availableClassrooms = new ArrayList<>(this.availableClassrooms); + copy.totalCourses = this.totalCourses; + copy.assignedCourses = this.assignedCourses; + copy.constraintViolations = this.constraintViolations; + + return copy; + } + + /** + * Adds an exam assignment to the schedule. + * Updates the quick lookup structures. + */ + public void addAssignment(ExamAssignment assignment) { + String courseCode = assignment.getCourseCode(); + assignments.put(courseCode, assignment); + totalCourses++; + + if (assignment.isAssigned()) { + updateLookupStructures(assignment, true); + assignedCourses++; + } + } + + /** + * Updates an existing assignment with new time slot and classroom. + */ + public boolean updateAssignment(String courseCode, TimeSlot timeSlot, String classroomId) { + ExamAssignment assignment = assignments.get(courseCode); + if (assignment == null) { + return false; + } + + if (assignment.isLocked()) { + return false; // Cannot modify locked assignments + } + + // Remove old assignment from lookup structures + if (assignment.isAssigned()) { + updateLookupStructures(assignment, false); + assignedCourses--; + } + + // Update assignment + assignment.setTimeSlot(timeSlot); + assignment.setClassroomId(classroomId); + + // Add new assignment to lookup structures + if (assignment.isAssigned()) { + updateLookupStructures(assignment, true); + assignedCourses++; + } + + return true; + } + + /** + * Removes an assignment (resets it to unassigned state). + */ + public boolean removeAssignment(String courseCode) { + ExamAssignment assignment = assignments.get(courseCode); + if (assignment == null || assignment.isLocked()) { + return false; + } + + if (assignment.isAssigned()) { + updateLookupStructures(assignment, false); + assignedCourses--; + } + + assignment.setTimeSlot(null); + assignment.setClassroomId(null); + + return true; + } + + /** + * Updates the quick lookup structures when an assignment changes. + */ + private void updateLookupStructures(ExamAssignment assignment, boolean add) { + if (!assignment.isAssigned()) + return; + + String classroomId = assignment.getClassroomId(); + String timeSlotId = assignment.getTimeSlot().getId(); + String courseCode = assignment.getCourseCode(); + + if (add) { + classroomTimeSlotUsage.computeIfAbsent(classroomId, k -> new HashSet<>()).add(timeSlotId); + timeSlotCourseMapping.computeIfAbsent(timeSlotId, k -> new HashSet<>()).add(courseCode); + } else { + Set slots = classroomTimeSlotUsage.get(classroomId); + if (slots != null) { + slots.remove(timeSlotId); + } + + Set courses = timeSlotCourseMapping.get(timeSlotId); + if (courses != null) { + courses.remove(courseCode); + } + } + } + + /** + * Checks if a classroom is available at a given time slot. + */ + public boolean isClassroomAvailable(String classroomId, TimeSlot timeSlot) { + Set usedSlots = classroomTimeSlotUsage.get(classroomId); + if (usedSlots == null) { + return true; + } + + // Check for overlapping time slots + for (ExamAssignment assignment : assignments.values()) { + if (assignment.isAssigned() && + assignment.getClassroomId().equals(classroomId) && + assignment.getTimeSlot().overlapsWith(timeSlot)) { + return false; + } + } + + return true; + } + + /** + * Gets all courses scheduled at a specific time slot. + */ + public Set getCoursesAtTimeSlot(TimeSlot timeSlot) { + Set courses = new HashSet<>(); + for (ExamAssignment assignment : assignments.values()) { + if (assignment.isAssigned() && + assignment.getTimeSlot().overlapsWith(timeSlot)) { + courses.add(assignment.getCourseCode()); + } + } + return courses; + } + + /** + * Gets all unassigned courses. + */ + public List getUnassignedCourses() { + List unassigned = new ArrayList<>(); + for (ExamAssignment assignment : assignments.values()) { + if (!assignment.isAssigned()) { + unassigned.add(assignment); + } + } + return unassigned; + } + + /** + * Checks if the schedule is complete (all courses are assigned). + */ + public boolean isComplete() { + return assignedCourses == totalCourses; + } + + /** + * Gets the completion percentage. + */ + public double getCompletionPercentage() { + if (totalCourses == 0) + return 100.0; + return (assignedCourses * 100.0) / totalCourses; + } + + // Getters and Setters + + public Map getAssignments() { + return Collections.unmodifiableMap(assignments); + } + + public ExamAssignment getAssignment(String courseCode) { + return assignments.get(courseCode); + } + + public List getAvailableTimeSlots() { + return availableTimeSlots; + } + + public void setAvailableTimeSlots(List availableTimeSlots) { + this.availableTimeSlots = availableTimeSlots; + } + + public List getAvailableClassrooms() { + return availableClassrooms; + } + + public void setAvailableClassrooms(List availableClassrooms) { + this.availableClassrooms = availableClassrooms; + } + + public int getTotalCourses() { + return totalCourses; + } + + public int getAssignedCourses() { + return assignedCourses; + } + + public int getConstraintViolations() { + return constraintViolations; + } + + public void setConstraintViolations(int constraintViolations) { + this.constraintViolations = constraintViolations; + } + + @Override + public String toString() { + return String.format("ScheduleState[%d/%d assigned, %.1f%% complete, %d violations]", + assignedCourses, totalCourses, getCompletionPercentage(), constraintViolations); + } +} diff --git a/src/main/java/org/example/se302/model/TimeSlot.java b/src/main/java/org/example/se302/model/TimeSlot.java new file mode 100644 index 0000000..e3b0f2f --- /dev/null +++ b/src/main/java/org/example/se302/model/TimeSlot.java @@ -0,0 +1,109 @@ +package org.example.se302.model; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +/** + * Represents a time slot for exam scheduling. + * A time slot is defined by a date and a specific time period. + */ +public class TimeSlot implements Comparable { + private LocalDate date; + private LocalTime startTime; + private LocalTime endTime; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + + public TimeSlot(LocalDate date, LocalTime startTime, LocalTime endTime) { + this.date = date; + this.startTime = startTime; + this.endTime = endTime; + } + + /** + * Creates a time slot with a default duration of 2 hours. + */ + public TimeSlot(LocalDate date, LocalTime startTime) { + this(date, startTime, startTime.plusHours(2)); + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public LocalTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalTime startTime) { + this.startTime = startTime; + } + + public LocalTime getEndTime() { + return endTime; + } + + public void setEndTime(LocalTime endTime) { + this.endTime = endTime; + } + + /** + * Checks if this time slot overlaps with another time slot. + * Two time slots overlap if they are on the same day and their time ranges + * intersect. + */ + public boolean overlapsWith(TimeSlot other) { + if (!this.date.equals(other.date)) { + return false; + } + // Check if time ranges overlap + return this.startTime.isBefore(other.endTime) && other.startTime.isBefore(this.endTime); + } + + /** + * Returns a unique identifier for this time slot. + */ + public String getId() { + return date.format(DATE_FORMATTER) + "_" + startTime.format(TIME_FORMATTER); + } + + @Override + public int compareTo(TimeSlot other) { + int dateCompare = this.date.compareTo(other.date); + if (dateCompare != 0) { + return dateCompare; + } + return this.startTime.compareTo(other.startTime); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TimeSlot timeSlot = (TimeSlot) o; + return Objects.equals(date, timeSlot.date) && + Objects.equals(startTime, timeSlot.startTime) && + Objects.equals(endTime, timeSlot.endTime); + } + + @Override + public int hashCode() { + return Objects.hash(date, startTime, endTime); + } + + @Override + public String toString() { + return date.format(DATE_FORMATTER) + " " + + startTime.format(TIME_FORMATTER) + "-" + + endTime.format(TIME_FORMATTER); + } +} diff --git a/src/main/resources/org/example/se302/view/import-view.fxml b/src/main/resources/org/example/se302/view/import-view.fxml index aae16cf..3e7a95b 100644 --- a/src/main/resources/org/example/se302/view/import-view.fxml +++ b/src/main/resources/org/example/se302/view/import-view.fxml @@ -4,73 +4,76 @@ - - - - + + + + + - -