feat: implement CSP algorithm architecture for exam scheduling

This commit is contained in:
feyzagereme
2025-12-12 22:35:18 +03:00
parent 1b2e447aae
commit 2db5e22c6b
9 changed files with 1294 additions and 65 deletions

View File

@@ -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<Constraint> hardConstraints;
private List<Constraint> softConstraints;
private Map<String, Course> courses;
private Map<String, Classroom> classrooms;
private List<TimeSlot> 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<String, Set<String>> 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<TimeSlot> timeSlots) {
this.availableTimeSlots = new ArrayList<>(timeSlots);
Collections.sort(this.availableTimeSlots);
}
/**
* Sets the available classrooms.
*/
public void setClassrooms(Map<String, Classroom> classrooms) {
this.classrooms = classrooms;
}
/**
* Sets the courses to schedule.
*/
public void setCourses(Map<String, Course> 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<String> courseCodes = new ArrayList<>(courses.keySet());
for (int i = 0; i < courseCodes.size(); i++) {
Course course1 = courses.get(courseCodes.get(i));
Set<String> 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<DomainValue> 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<String> conflicts = conflictGraph.get(courseCode);
return conflicts != null ? conflicts.size() : 0;
}
/**
* Orders domain values using Least Constraining Value (LCV) heuristic.
*/
private List<DomainValue> orderDomainValues(ExamAssignment assignment, ScheduleState state) {
List<DomainValue> 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<String> 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<String> coursesAtSameTime = state.getCoursesAtTimeSlot(timeSlot);
Set<String> 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<String, Set<String>> getConflictGraph() {
return Collections.unmodifiableMap(conflictGraph);
}
}

View File

@@ -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<String, Classroom> classrooms;
public CapacityConstraint(Map<String, Classroom> 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<String, Classroom> classrooms) {
this.classrooms = classrooms;
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}

View File

@@ -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<String, Course> courses;
public StudentConflictConstraint(Map<String, Course> 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<String> enrolledStudents = new HashSet<>(course.getEnrolledStudents());
// Get all courses scheduled at overlapping time slots
Set<String> 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<String> enrolledStudents = new HashSet<>(course.getEnrolledStudents());
Set<String> 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<String> 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<String, Course> courses) {
this.courses = courses;
}
/**
* Gets the set of courses that conflict with the given course
* (i.e., share at least one student).
*/
public Set<String> getConflictingCourses(String courseCode) {
Set<String> conflicting = new HashSet<>();
Course course = courses.get(courseCode);
if (course == null) {
return conflicting;
}
Set<String> enrolledStudents = new HashSet<>(course.getEnrolledStudents());
for (Map.Entry<String, Course> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<String, ExamAssignment> assignments;
// Quick lookup structures for constraint checking
private Map<String, Set<String>> classroomTimeSlotUsage; // classroomId -> set of timeSlot IDs
private Map<String, Set<String>> timeSlotCourseMapping; // timeSlotId -> set of course codes
// Available resources
private List<TimeSlot> availableTimeSlots;
private List<Classroom> 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<String, ExamAssignment> entry : this.assignments.entrySet()) {
copy.assignments.put(entry.getKey(), entry.getValue().copy());
}
for (Map.Entry<String, Set<String>> entry : this.classroomTimeSlotUsage.entrySet()) {
copy.classroomTimeSlotUsage.put(entry.getKey(), new HashSet<>(entry.getValue()));
}
for (Map.Entry<String, Set<String>> 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<String> slots = classroomTimeSlotUsage.get(classroomId);
if (slots != null) {
slots.remove(timeSlotId);
}
Set<String> 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<String> 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<String> getCoursesAtTimeSlot(TimeSlot timeSlot) {
Set<String> 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<ExamAssignment> getUnassignedCourses() {
List<ExamAssignment> 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<String, ExamAssignment> getAssignments() {
return Collections.unmodifiableMap(assignments);
}
public ExamAssignment getAssignment(String courseCode) {
return assignments.get(courseCode);
}
public List<TimeSlot> getAvailableTimeSlots() {
return availableTimeSlots;
}
public void setAvailableTimeSlots(List<TimeSlot> availableTimeSlots) {
this.availableTimeSlots = availableTimeSlots;
}
public List<Classroom> getAvailableClassrooms() {
return availableClassrooms;
}
public void setAvailableClassrooms(List<Classroom> 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);
}
}

View File

@@ -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<TimeSlot> {
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);
}
}

View File

@@ -4,10 +4,12 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns="http://javafx.com/javafx"
<ScrollPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.example.se302.controller.ImportController"
spacing="20" styleClass="content-pane">
fitToWidth="true" fitToHeight="true"
hbarPolicy="AS_NEEDED" vbarPolicy="AS_NEEDED">
<VBox spacing="20" styleClass="content-pane">
<padding>
<Insets top="20" right="20" bottom="20" left="20"/>
</padding>
@@ -74,3 +76,4 @@
<Button text="Clear All" onAction="#onClearAll"/>
</HBox>
</VBox>
</ScrollPane>