diff --git a/src/main/java/com/uh/rainbow/controller/CampusController.java b/src/main/java/com/uh/rainbow/controller/CampusController.java
index 767404c..2fe7775 100644
--- a/src/main/java/com/uh/rainbow/controller/CampusController.java
+++ b/src/main/java/com/uh/rainbow/controller/CampusController.java
@@ -1,9 +1,10 @@
package com.uh.rainbow.controller;
import com.uh.rainbow.dto.course.CourseDTO;
-import com.uh.rainbow.dto.identifier.IdentifierDTO;
import com.uh.rainbow.dto.response.*;
+import com.uh.rainbow.entities.Section;
import com.uh.rainbow.service.HTMLParserService;
+import com.uh.rainbow.services.DTOMapperService;
import com.uh.rainbow.util.SourceURL;
import com.uh.rainbow.util.filter.CourseFilter;
import com.uh.rainbow.util.logging.Logger;
@@ -14,26 +15,23 @@
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
-import java.time.Instant;
-import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
/**
* File: CampusController.java
*
- * Description: Controller that handles request for course and subject information at UH campuses
+ * Description: Controller that handles parsing campus and course information
*
* @author Derek Garcia
*/
@RequestMapping("/v1/campuses")
-@RestController(value = "CampusController")
+@RestController(value = "campusController")
public class CampusController {
private final static Logger LOGGER = new Logger(CampusController.class);
private final HTMLParserService htmlParserService = new HTMLParserService();
+ private final DTOMapperService dtoMapperService = new DTOMapperService();
/**
* Util logging method for reporting HTTP failures
@@ -176,7 +174,8 @@ public ResponseEntity getCourses(
.setKeywords(keyword)
.build();
// Get all courses for subject
- List courseDTOs = this.htmlParserService.parseCourses(cf, instID, termID, subjectID);
+ List sections = this.htmlParserService.parseSections(cf, instID, termID, subjectID);
+ List courseDTOs = this.dtoMapperService.toCourseDTOs(sections);
return new ResponseEntity<>(
new CourseResponseDTO(courseDTOs),
HttpStatus.OK
@@ -226,10 +225,6 @@ public ResponseEntity getCourses(
@RequestParam(required = false) List instructor,
@RequestParam(required = false) List keyword) {
try {
- // Get all available subjects
- Instant start = Instant.now();
- List subjects = this.htmlParserService.parseSubjects(instID, termID);
-
// Build filter
CourseFilter cf = new CourseFilter.Builder()
.setCRNs(crn)
@@ -244,54 +239,11 @@ public ResponseEntity getCourses(
.setKeywords(keyword)
.build();
- // Parse each subject for courses
- List failedSources = new ArrayList<>();
- List>> futures = new ArrayList<>();
- for (IdentifierDTO s : subjects) {
-
- // skip if not in filter
- if (!cf.validSubject(s.id()))
- continue;
-
- // Add async job to queue
- SourceURL source = new SourceURL(instID, termID, s.id());
- futures.add(CompletableFuture
- .supplyAsync(() -> {
- try {
- // Attempt to parse
- return this.htmlParserService.parseCourses(cf, instID, termID, s.id());
- } catch (HttpStatusException e) {
- // Report html access failure, add to failed sources and continue
- reportHTTPAccessError(MessageBuilder.Type.COURSE, e);
- LOGGER.warn(new MessageBuilder(MessageBuilder.Type.COURSE).addDetails("Skipping %s".formatted(source)));
- failedSources.add(source.toString());
- } catch (IOException e) {
- // Internal server error, add to failed sources and continue
- LOGGER.error(new MessageBuilder(MessageBuilder.Type.COURSE).addDetails(e));
- failedSources.add(source.toString());
- }
- return new ArrayList<>(); // empty results
- }));
- }
- // Join each thread / wait for each to finish
- futures.forEach(CompletableFuture::join);
-
- // Get all results
- List courseDTOs = new ArrayList<>();
- for (CompletableFuture> result : futures) {
- try {
- courseDTOs.addAll(result.get());
- } catch (ExecutionException | InterruptedException e) {
- LOGGER.error(new MessageBuilder(MessageBuilder.Type.COURSE).addDetails(e));
- }
- }
+ // Parse Sections
+ List sections = this.htmlParserService.parseSections(cf, instID, termID);
+ List courseDTOs = this.dtoMapperService.toCourseDTOs(sections);
- // Report Success and return results
- int numSites = futures.size();
- LOGGER.info(new MessageBuilder(MessageBuilder.Type.COURSE)
- .addDetails("Parsed %s site%s".formatted(numSites, numSites == 1 ? "" : "s"))
- .setDuration(start));
- return new ResponseEntity<>(new CourseResponseDTO(courseDTOs, failedSources), HttpStatus.OK);
+ return new ResponseEntity<>(new CourseResponseDTO(courseDTOs), HttpStatus.OK);
} catch (HttpStatusException e) {
// Report and return html access failure
reportHTTPAccessError(MessageBuilder.Type.COURSE, e);
diff --git a/src/main/java/com/uh/rainbow/controller/ParserController.java b/src/main/java/com/uh/rainbow/controller/ParserController.java
deleted file mode 100644
index d1d1f6b..0000000
--- a/src/main/java/com/uh/rainbow/controller/ParserController.java
+++ /dev/null
@@ -1,262 +0,0 @@
-package com.uh.rainbow.controller;
-
-import com.uh.rainbow.dto.course.CourseDTO;
-import com.uh.rainbow.dto.identifier.IdentifierDTO;
-import com.uh.rainbow.dto.response.*;
-import com.uh.rainbow.entities.Section;
-import com.uh.rainbow.services.DTOMapperService;
-import com.uh.rainbow.service.HTMLParserService;
-import com.uh.rainbow.util.SourceURL;
-import com.uh.rainbow.util.filter.CourseFilter;
-import com.uh.rainbow.util.logging.Logger;
-import com.uh.rainbow.util.logging.MessageBuilder;
-import org.jsoup.HttpStatusException;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-
-import java.io.IOException;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-
-/**
- * File: ParserController.java
- *
- * Description: Main controller for Rainbow API
- *
- * @author Derek Garcia
- */
-
-@RequestMapping("/v1/campuses")
-@RestController(value = "parserController")
-public class ParserController {
-
- private final static Logger LOGGER = new Logger(ParserController.class);
- private final HTMLParserService htmlParserService = new HTMLParserService();
- private final DTOMapperService dtoMapperService = new DTOMapperService();
-
- /**
- * Util logging method for reporting HTTP failures
- *
- * @param type Log type
- * @param e HttpStatusException
- */
- private void reportHTTPAccessError(MessageBuilder.Type type, HttpStatusException e) {
- MessageBuilder mb = new MessageBuilder(type)
- .addDetails("Failed to fetch HTML")
- .addDetails(e.getStatusCode());
- LOGGER.warn(mb);
- LOGGER.debug(mb.addDetails(e));
- }
-
- /**
- * GET Endpoint: /campuses
- * Get list of University of Hawaii Campuses
- *
- * @return List of University of Hawaii Campuses and their ID's
- */
- @GetMapping(value = "")
- public ResponseEntity getAllCampuses() {
- try {
- // Get all campuses
- return new ResponseEntity<>(
- new IdentifierResponseDTO(new SourceURL(), this.htmlParserService.parseInstitutions()),
- HttpStatus.OK
- );
- } catch (HttpStatusException e) {
- // Report and return html access failure
- reportHTTPAccessError(MessageBuilder.Type.INST, e);
- return new ResponseEntity<>(new BadAccessResponseDTO(e), HttpStatus.BAD_REQUEST);
- } catch (IOException e) {
- // Internal server error
- LOGGER.error(new MessageBuilder(MessageBuilder.Type.INST).addDetails(e));
- return new ResponseEntity<>(new APIErrorResponseDTO(e), HttpStatus.INTERNAL_SERVER_ERROR);
- }
- }
-
-
- /**
- * GET Endpoint: /campuses/{instID}/terms
- * Get list of terms for a campus
- *
- * @param instID Inst ID to search for terms
- * @return List of term names and their ID's
- */
- @GetMapping(value = "/{instID}/terms")
- public ResponseEntity getAllTerms(@PathVariable String instID) {
- try {
- // Get all terms
- return new ResponseEntity<>(
- new IdentifierResponseDTO(new SourceURL(instID), this.htmlParserService.parseTerms(instID)),
- HttpStatus.OK
- );
- } catch (HttpStatusException e) {
- // Report and return html access failure
- reportHTTPAccessError(MessageBuilder.Type.TERM, e);
- return new ResponseEntity<>(new BadAccessResponseDTO(e), HttpStatus.BAD_REQUEST);
- } catch (IOException e) {
- // Internal server error
- LOGGER.error(new MessageBuilder(MessageBuilder.Type.TERM).addDetails(e));
- return new ResponseEntity<>(new APIErrorResponseDTO(e), HttpStatus.INTERNAL_SERVER_ERROR);
- }
- }
-
-
- /**
- * GET Endpoint: /campuses/{instID}/terms/{termID}/subjects
- * Get list of subjects for a given campus and term
- *
- * @param instID Inst ID to search for subjects
- * @param termID Term ID to search for subjects
- * @return List of subjects for a given campus and term
- */
- @GetMapping(value = "/{instID}/terms/{termID}/subjects")
- public ResponseEntity getSubjects(@PathVariable String instID, @PathVariable String termID) {
- try {
- // Get all subjects
- return new ResponseEntity<>(
- new IdentifierResponseDTO(new SourceURL(instID, termID), this.htmlParserService.parseSubjects(instID, termID)),
- HttpStatus.OK
- );
- } catch (HttpStatusException e) {
- // Report and return html access failure
- reportHTTPAccessError(MessageBuilder.Type.SUBJECT, e);
- return new ResponseEntity<>(new BadAccessResponseDTO(e), HttpStatus.BAD_REQUEST);
- } catch (IOException e) {
- // Internal server error
- LOGGER.error(new MessageBuilder(MessageBuilder.Type.SUBJECT).addDetails(e));
- return new ResponseEntity<>(new APIErrorResponseDTO(e), HttpStatus.INTERNAL_SERVER_ERROR);
- }
- }
-
- /**
- * GET Endpoint: /campuses/{instID}/terms/{termID}/subjects/{subjectID}
- * Get all courses for a given campus, term, and subject
- * Best used for finding courses for a single subject
- *
- * @param instID Inst ID to search for courses
- * @param termID Term ID to search for courses
- * @param subjectID Subject ID to search for courses
- * @param crn List of Course Reference Numbers to filter by
- * @param code List of course codes to filter by. '*' wild card can be used ie 1** -> 101, 102, 110 etc
- * @param start_after Earliest time a class can start in 24hr format
- * @param end_before Latest time a class can run in 24hr format
- * @param online Only classes online sections
- * @param sync Only synchronous sections
- * @param day UH day of week codes to filter by. Append with '!' to inverse search ie !M -> get all sections not on Monday
- * @param instructor Instructors to filter by. Append with '!' to inverse search ie !foo -> get all sections that don't have instructor 'foo'
- * @param keyword Keywords to filter course names by. Append with '!' to inverse search ie !foo -> get all courses that don't have 'foo' in the name
- * @return List of courses for a given campus, term, and subject that pass filters
- */
- @GetMapping(value = "/{instID}/terms/{termID}/subjects/{subjectID}")
- public ResponseEntity getCourses(
- @PathVariable String instID,
- @PathVariable String termID,
- @PathVariable String subjectID,
- @RequestParam(required = false) List crn,
- @RequestParam(required = false) List code,
- @RequestParam(required = false) String start_after,
- @RequestParam(required = false) String end_before,
- @RequestParam(required = false) String online,
- @RequestParam(required = false) String sync,
- @RequestParam(required = false) List day,
- @RequestParam(required = false) List instructor,
- @RequestParam(required = false) List keyword) {
- try {
- // Build filter
- CourseFilter cf = new CourseFilter.Builder()
- .setCRNs(crn)
- .setCourseNumbers(code)
- .setStartAfter(start_after)
- .setEndBefore(end_before)
- .setOnline(online)
- .setSynchronous(sync)
- .setDays(day)
- .setInstructors(instructor)
- .setKeywords(keyword)
- .build();
- // Get all courses for subject
- List sections = this.htmlParserService.parseSections(cf, instID, termID, subjectID);
- List courseDTOs = this.dtoMapperService.toCourseDTOs(sections);
- return new ResponseEntity<>(
- new CourseResponseDTO(courseDTOs),
- HttpStatus.OK
- );
- } catch (HttpStatusException e) {
- // Report and return html access failure
- reportHTTPAccessError(MessageBuilder.Type.SUBJECT, e);
- return new ResponseEntity<>(new BadAccessResponseDTO(e), HttpStatus.BAD_REQUEST);
- } catch (IOException e) {
- // Internal Server Error
- LOGGER.error(new MessageBuilder(MessageBuilder.Type.SUBJECT).addDetails(e));
- return new ResponseEntity<>(new APIErrorResponseDTO(e), HttpStatus.INTERNAL_SERVER_ERROR);
- }
- }
-
- /**
- * GET Endpoint: /campuses/{instID}/terms/{termID}/courses
- * Get all courses for a given campus and term
- * Best used for finding courses for multiple subjects
- *
- * @param instID Inst ID to search for courses
- * @param termID Term ID to search for courses
- * @param sub List of Subjects to filter by
- * @param crn List of Course Reference Numbers to filter by
- * @param code List of course codes to filter by. '*' wild card can be used ie 1** -> 101, 102, 110 etc
- * @param start_after Earliest time a class can start in 24hr format
- * @param end_before Latest time a class can run in 24hr format
- * @param online Only classes online sections
- * @param sync Only synchronous sections
- * @param day UH day of week codes to filter by. Append with '!' to inverse search ie !M -> get all sections not on Monday
- * @param instructor Instructors to filter by. Append with '!' to inverse search ie !foo -> get all sections that don't have instructor 'foo'
- * @param keyword Keywords to filter course names by. Append with '!' to inverse search ie !foo -> get all courses that don't have 'foo' in the name
- * @return List of courses for a given campus and term that pass filters
- */
- @GetMapping(value = "/{instID}/terms/{termID}/courses")
- public ResponseEntity getCourses(
- @PathVariable String instID,
- @PathVariable String termID,
- @RequestParam(required = false) List crn,
- @RequestParam(required = false) List sub,
- @RequestParam(required = false) List code,
- @RequestParam(required = false) String start_after,
- @RequestParam(required = false) String end_before,
- @RequestParam(required = false) String online,
- @RequestParam(required = false) String sync,
- @RequestParam(required = false) List day,
- @RequestParam(required = false) List instructor,
- @RequestParam(required = false) List keyword) {
- try {
- // Build filter
- CourseFilter cf = new CourseFilter.Builder()
- .setCRNs(crn)
- .setSubjects(sub)
- .setCourseNumbers(code)
- .setStartAfter(start_after)
- .setEndBefore(end_before)
- .setOnline(online)
- .setSynchronous(sync)
- .setDays(day)
- .setInstructors(instructor)
- .setKeywords(keyword)
- .build();
-
- // Parse Sections
- List sections = this.htmlParserService.parseSections(cf, instID, termID);
- List courseDTOs = this.dtoMapperService.toCourseDTOs(sections);
-
- return new ResponseEntity<>(new CourseResponseDTO(courseDTOs), HttpStatus.OK);
- } catch (HttpStatusException e) {
- // Report and return html access failure
- reportHTTPAccessError(MessageBuilder.Type.COURSE, e);
- return new ResponseEntity<>(new BadAccessResponseDTO(e), HttpStatus.BAD_REQUEST);
- } catch (IOException e) {
- // Internal Server error
- LOGGER.error(new MessageBuilder(MessageBuilder.Type.COURSE).addDetails(e));
- return new ResponseEntity<>(new APIErrorResponseDTO(e), HttpStatus.INTERNAL_SERVER_ERROR);
- }
- }
-}
diff --git a/src/main/java/com/uh/rainbow/dto/course/CourseDTO.java b/src/main/java/com/uh/rainbow/dto/course/CourseDTO.java
index d3002b6..b2f8599 100644
--- a/src/main/java/com/uh/rainbow/dto/course/CourseDTO.java
+++ b/src/main/java/com/uh/rainbow/dto/course/CourseDTO.java
@@ -1,8 +1,6 @@
package com.uh.rainbow.dto.course;
import com.uh.rainbow.dto.section.SectionDTO;
-import com.uh.rainbow.entities.Course;
-import com.uh.rainbow.entities.Section;
import com.uh.rainbow.util.SourceURL;
import java.util.ArrayList;
@@ -25,16 +23,4 @@ public record CourseDTO(String cid, String name, String source, String credits,
public CourseDTO(SourceURL source, String cid, String name, String credits) {
this(cid, name, source.toString(), credits, new ArrayList<>());
}
-
- /**
- * Convert stored Section DTOs into Section objects
- *
- * @return List of converted sections
- */
- public List toSections() {
- List sections = new ArrayList<>();
- Course course = new Course(this.cid, this.name, this.credits);
- this.sections.forEach((s) -> sections.add(new Section(course, s)));
- return sections;
- }
}
diff --git a/src/main/java/com/uh/rainbow/entities/Meeting.java b/src/main/java/com/uh/rainbow/entities/Meeting.java
index 479fcd7..5b3663e 100644
--- a/src/main/java/com/uh/rainbow/entities/Meeting.java
+++ b/src/main/java/com/uh/rainbow/entities/Meeting.java
@@ -40,44 +40,6 @@ private Meeting(Day day, SimpleTime startTime, SimpleTime endTime, SimpleDate st
this.room = room;
}
- /**
- * Create new Meeting object from a meeting DTO
- *
- * @param meetingDTO Meeting DTO to create meeting from
- * @throws ParseException Failed to parse date
- */
- public Meeting(MeetingDTO meetingDTO) throws ParseException {
- this.day = Day.toDay(meetingDTO.day());
- this.startTime = new SimpleTime(meetingDTO.start_time(), "");
- this.endTime = new SimpleTime(meetingDTO.end_time(), "");
- this.startDate = new SimpleDate(meetingDTO.start_date());
- this.endDate = new SimpleDate(meetingDTO.end_date());
- this.room = meetingDTO.room();
- }
-
- /**
- * Create new meetings parsed from UH style input parameters
- *
- * @param dayString Day string formatted D*
- * @param timeString Time formatted HHmm-HHmm(?:a|p)
- * @param roomString Room name
- * @param dateString Date formatted DD/MM(?:|-DD/MM)
- * @return List of parsed meetings
- * @throws ParseException Fail to parse time or day block
- */
- public static List createMeetings(String dayString, String timeString, String roomString, String dateString) throws ParseException {
- List meetings = new ArrayList<>();
-
- List days = Day.toDays(dayString);
-
- TimeBlock tb = new TimeBlock(timeString, dateString);
- days.forEach((day) -> meetings.add(
- new Meeting(day, tb.getStartTime(), tb.getEndTime(), tb.getStartDate(), tb.getEndDate(), roomString))
- );
-
- return meetings;
- }
-
/**
* Determine if this meeting conflicts with another meeting
*
diff --git a/src/main/java/com/uh/rainbow/entities/PotentialSchedule.java b/src/main/java/com/uh/rainbow/entities/PotentialSchedule.java
index 3d0dddb..ec69e55 100644
--- a/src/main/java/com/uh/rainbow/entities/PotentialSchedule.java
+++ b/src/main/java/com/uh/rainbow/entities/PotentialSchedule.java
@@ -9,12 +9,13 @@
* File: PotentialSchedule.java
*
* Description: Collection of courses / sections that represent a potential schedule
+ * TODO Add finalized "read only" schedule?
*
* @author Derek Garcia
*/
public class PotentialSchedule {
- private final Set courses;
- private final Set sections;
+ private Set courses = new HashSet<>();
+ private Set sections = new HashSet<>();
private final List remainingSections;
/**
@@ -23,9 +24,7 @@ public class PotentialSchedule {
* @param remainingSections Remaining sections to that can potentially be included in this schedule
*/
public PotentialSchedule(List remainingSections) {
- this.courses = new HashSet<>();
- this.sections = new HashSet<>();
- this.remainingSections = remainingSections;
+ this.remainingSections = new ArrayList<>(remainingSections);
}
/**
@@ -37,15 +36,17 @@ public PotentialSchedule(List remainingSections) {
private PotentialSchedule(PotentialSchedule other, Section next) {
// Copy current section and add next section
this.courses = new HashSet<>(other.courses);
- this.courses.add(next.getCourse());
+ this.courses.add(next.getCID());
// Copy current section and add next section
this.sections = new HashSet<>(other.sections);
this.sections.add(next);
- // Copy remaining sections and remove all sections for same course of the next section
- this.remainingSections = new ArrayList<>(other.remainingSections);
- this.remainingSections.removeIf((s) -> s.getCourse().equals(next.getCourse()));
+ // Copy remaining sections of missing courses
+ this.remainingSections = new ArrayList<>(
+ other.remainingSections.stream()
+ .filter((s) -> !this.courses.contains(s.getCID()))
+ .toList());
}
/**
@@ -54,19 +55,19 @@ private PotentialSchedule(PotentialSchedule other, Section next) {
* @param courses List of courses that the schedule must contain to be considered complete
* @return True if complete, false otherwise
*/
- public boolean isComplete(Set courses) {
+ public boolean isComplete(Set courses) {
return this.courses.containsAll(courses);
}
/**
* Compares this schedule to another schedule. Schedules are considered "equal"
- * if they both contain all the same courses
+ * if they both contain all the same sections
*
* @param other Other potential schedule to compare
* @return True if equal, False otherwise
*/
public boolean isEquals(PotentialSchedule other) {
- return isComplete(other.getCourses());
+ return this.sections.size() == other.getSections().size() && this.sections.containsAll(other.getSections());
}
/**
@@ -86,16 +87,16 @@ public List getSuccessors() {
return successors;
}
+
/**
- * Get courses this schedule has
- *
- * @return Set of courses
+ * @return Set of sections in this schedule
*/
- public Set getCourses() {
- // Not sure why done like this, maybe b/c didn't have courses at one point. Keeping just in case
- // Set courses = new HashSet<>();
- // this.remainingSections.forEach( (s) -> courses.add(s.getCourse()) );
- // return courses;
- return this.courses;
+ public Set getSections() {
+ return this.sections;
+ }
+
+ @Override
+ public String toString() {
+ return String.join(", ", this.sections.stream().map(Section::toString).toList());
}
}
diff --git a/src/main/java/com/uh/rainbow/entities/Section.java b/src/main/java/com/uh/rainbow/entities/Section.java
index c473795..68574e3 100644
--- a/src/main/java/com/uh/rainbow/entities/Section.java
+++ b/src/main/java/com/uh/rainbow/entities/Section.java
@@ -2,10 +2,10 @@
import com.uh.rainbow.util.SourceURL;
-import java.text.ParseException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
+import java.util.Objects;
/**
* File: Section.java
@@ -26,7 +26,7 @@ public class Section {
private final int seatsAvailable;
private final List meetings = new ArrayList<>();
private int failedMeetings = 0; // Assume no failed meetings
- private List additionalDetails = new ArrayList<>();
+ private final List additionalDetails = new ArrayList<>();
/**
* Create new Section
@@ -53,29 +53,6 @@ public Section(SourceURL source, int crn, String cid, String sectionNumber, Stri
this.seatsAvailable = seatsAvailable;
}
- /**
- * Create a new Section from a section DTO
- *
- * @param course Course the section belongs to
- * @param sectionDTO SectionDTO to convert
- */
- public Section(Course course, SectionDTO sectionDTO) {
- this.crn = sectionDTO.crn();
- this.course = course;
- this.sid = sectionDTO.sid();
- this.instructor = sectionDTO.instructor();
- this.currEnrolled = sectionDTO.curr_enrolled();
- this.seatsAvailable = sectionDTO.seats_available();
- this.additionalDetails = sectionDTO.additional_details();
- sectionDTO.meetings().forEach((m) -> {
- try {
- this.meetings.add(new Meeting(m));
- } catch (ParseException e) {
- throw new RuntimeException(e);
- }
- });
- }
-
/**
* Check to see if this section has any conflicts with another section
*
@@ -124,7 +101,7 @@ public int getFailedMeetings() {
/**
* @return Source URL data was parsed from
*/
- public SourceURL getSourceURL(){
+ public SourceURL getSourceURL() {
return this.sourceURL;
}
@@ -204,4 +181,23 @@ public List getAdditionalDetails() {
public List getMeetings() {
return this.meetings;
}
+
+
+ @Override
+ public String toString() {
+ return "%s:%s".formatted(this.cid, this.sectionNumber);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Section section = (Section) o;
+ return Objects.equals(cid, section.cid) && Objects.equals(sectionNumber, section.sectionNumber);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(cid, sectionNumber);
+ }
}
diff --git a/src/main/java/com/uh/rainbow/entities/time/TimeBlock.java b/src/main/java/com/uh/rainbow/entities/time/TimeBlock.java
index a9a3ac2..4805d4c 100644
--- a/src/main/java/com/uh/rainbow/entities/time/TimeBlock.java
+++ b/src/main/java/com/uh/rainbow/entities/time/TimeBlock.java
@@ -55,7 +55,7 @@ public TimeBlock(String timeString, String dateString) throws ParseException {
this.startTime.addHours(12);
// Mark end in afternoon if 'p'
- if (this.endTime.beforeOrEqual(new SimpleTime("1200")) == 1)
+ if (this.endTime.beforeOrEqual(new SimpleTime("1159")) == 1)
this.endTime.addHours(12);
// If the class is longer than 5 hours, start is probably in am
diff --git a/src/main/java/com/uh/rainbow/entities/time/simple/SimpleTime.java b/src/main/java/com/uh/rainbow/entities/time/simple/SimpleTime.java
index b40b18d..c68a0ef 100644
--- a/src/main/java/com/uh/rainbow/entities/time/simple/SimpleTime.java
+++ b/src/main/java/com/uh/rainbow/entities/time/simple/SimpleTime.java
@@ -28,18 +28,6 @@ public SimpleTime(String time) throws ParseException {
super(TIME_INPUT_FORMAT, time); // 1/1/70 HH:mm
}
- /**
- * Create a simple time from MeetingDTO
- * TODO find better way to implement
- *
- * @param time time in the format of 'hh:mm a' ( 09:00 am, 02:30 pm, etc )
- * @param ignored Ignored placeholder to prevent ambiguous constructor
- * @throws ParseException Failed to parse time string
- */
- public SimpleTime(String time, String ignored) throws ParseException {
- super(OUTPUT_FORMAT, time);
- }
-
/**
* Add hours to this Simple Time
*
diff --git a/src/main/java/com/uh/rainbow/service/SchedulerService.java b/src/main/java/com/uh/rainbow/service/SchedulerService.java
index 1a12a5b..fed9cf4 100644
--- a/src/main/java/com/uh/rainbow/service/SchedulerService.java
+++ b/src/main/java/com/uh/rainbow/service/SchedulerService.java
@@ -1,11 +1,12 @@
package com.uh.rainbow.service;
-import com.uh.rainbow.dto.course.CourseDTO;
-import com.uh.rainbow.entities.Course;
import com.uh.rainbow.entities.PotentialSchedule;
import com.uh.rainbow.entities.Section;
+import com.uh.rainbow.util.logging.Logger;
+import com.uh.rainbow.util.logging.MessageBuilder;
import org.springframework.stereotype.Service;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -20,60 +21,81 @@
*/
@Service
public class SchedulerService {
-
- /**
- * Generate list of valid schedules
- *
- * @param courses List of courses to include in the schedule
- * @return List of valid schedules
- */
- public List schedule(List courses) {
- // Convert course DTO back into courses
- List cor = new ArrayList<>();
- courses.forEach((c) -> cor.add(new Course(c.cid(), c.name(), c.credits())));
-
- // Convert section DTO back into sections
- List sections = new ArrayList<>();
- courses.forEach((c) -> sections.addAll(c.toSections())); // todo add buffers
-
- // Generate all possible schedules
- return new Scheduler(cor).solve(new PotentialSchedule(sections));
- }
+ public static final Logger LOGGER = new Logger(SchedulerService.class);
/**
* Scheduler that generates valid schedules
*/
private static class Scheduler {
- private final Set requiredCourses;
+ private final Set requiredCourses = new HashSet<>();
+ private final PotentialSchedule seed;
private final List results = new ArrayList<>();
/**
* Create a new Scheduler
*
- * @param requiredCourses List of courses that must be in the final schedule
+ * @param sections Initial pool of sections to use
*/
- public Scheduler(List requiredCourses) {
- this.requiredCourses = new HashSet<>(requiredCourses);
+ public Scheduler(List sections) {
+ sections.forEach((s) -> this.requiredCourses.add(s.getCID()));
+ this.seed = new PotentialSchedule(sections);
}
/**
* Attempt to solve a partially completed potentialSchedule
*
* @param potentialSchedule Starting potentialSchedule to complete
- * @return List of valid schedules
*/
- public List solve(PotentialSchedule potentialSchedule) {
+ private void solve(PotentialSchedule potentialSchedule) {
// Add new potentialSchedule if has all courses and the result doesn't contain an equivalent potentialSchedule
- if (potentialSchedule.isComplete(this.requiredCourses) && this.results.stream().noneMatch((s) -> s.isEquals(potentialSchedule)))
+ if (potentialSchedule.isComplete(this.requiredCourses) && this.results.stream().noneMatch((s) -> s.isEquals(potentialSchedule))) {
+ LOGGER.info(new MessageBuilder(MessageBuilder.Type.SCHEDULE)
+ .addDetails("Found new schedule")
+ .addDetails(potentialSchedule)
+ );
this.results.add(potentialSchedule);
+ }
// Solve each successor potentialSchedule
+ if(!potentialSchedule.getSections().isEmpty())
+ LOGGER.info(new MessageBuilder(MessageBuilder.Type.SCHEDULE).addDetails("Attempting to solve " + potentialSchedule));
potentialSchedule.getSuccessors().forEach(this::solve);
// When reach here, all potential solutions have been found
+ if(!potentialSchedule.getSections().isEmpty())
+ LOGGER.info(new MessageBuilder(MessageBuilder.Type.SCHEDULE).addDetails("All schedules exhausted for " + potentialSchedule));
+ }
+
+ /**
+ * Entrypoint to recursive solver using initial values
+ *
+ * @return List of valid potential schedules found
+ */
+ public List solve() {
+ // Solve seed and return results
+ Instant start = Instant.now();
+ solve(this.seed);
+ // Log findings
+ MessageBuilder mb = new MessageBuilder(MessageBuilder.Type.SCHEDULE).setDuration(start);
+ if (this.results.isEmpty()) {
+ LOGGER.warn(mb.addDetails("No valid schedules found"));
+ } else {
+ LOGGER.info(mb.addDetails("Found %s schedule%s".formatted(this.results.size(), this.results.size() == 1 ? "" : "s")));
+ }
+
return this.results;
}
}
+ /**
+ * Generate list of valid schedules
+ *
+ * @return List of valid schedules
+ */
+ public List schedule(List sections) {
+ // Generate all possible schedules
+ return new Scheduler(sections).solve();
+ }
+
}
diff --git a/src/main/java/com/uh/rainbow/util/filter/CourseFilter.java b/src/main/java/com/uh/rainbow/util/filter/CourseFilter.java
index 00573c8..e72d13c 100644
--- a/src/main/java/com/uh/rainbow/util/filter/CourseFilter.java
+++ b/src/main/java/com/uh/rainbow/util/filter/CourseFilter.java
@@ -107,7 +107,6 @@ public Builder setFullCourses(List fullCourses){
this.fullCourses = new FullCoursePattern(new HashSet<>(), Pattern.compile(regex, Pattern.CASE_INSENSITIVE));
// Save all subjects to filter
- fullCourses.replaceAll(String::toUpperCase);
Pattern subject = Pattern.compile("([a-z]*)", Pattern.CASE_INSENSITIVE);
for(String course : fullCourses){
Matcher m = subject.matcher(course);
@@ -486,182 +485,9 @@ public boolean validSubject(String subject) {
}
/**
- * Builder for Course Filter
+ * @return Set of valid subjects
*/
- public static class Builder {
- private Set crns;
- private Pattern codes;
- private Set subjects;
- private RegexFilter days;
- private SimpleTime startAfter;
- private SimpleTime endAfter;
- private int online = -1;
- private int synchronous = -1;
- private RegexFilter instructors;
- private RegexFilter keywords;
-
- /**
- * Set Course Reference numbers
- *
- * @param crns Course Reference numbers
- * @return CourseFilterBuilder
- */
- public Builder setCRNs(List crns) {
- if (crns != null)
- this.crns = new HashSet<>(crns);
- return this;
- }
-
- /**
- * Set course numbers ( 101, 301, etc )
- * Wild cards can also be used ie 1** will return and 100 level course
- *
- * @param codes Course numbers
- * @return CourseFilterBuilder
- */
- public Builder setCourseNumbers(List codes) {
- if (codes != null) {
- // replace * with regex numbers
- String regex = StringUtils.join(codes, "|")
- .replace("**", "[0-9]{2}")
- .replace("*", "[0-9]");
-
- this.codes = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
- }
- return this;
- }
-
- /**
- * Set subjects ( ICS, FIRE, etc )
- *
- * @param subjects Subjects
- * @return CourseFilterBuilder
- */
- public Builder setSubjects(List subjects) {
- if (subjects != null) {
- subjects.replaceAll(String::toUpperCase);
- this.subjects = new HashSet<>(subjects);
- }
- return this;
- }
-
- /**
- * Set list of days for meetings using UH day codes.
- * Prepending with '!' to inverse the search
- * ie "!M" -> sections not on monday
- *
- * @param days list of days to filter by
- * @return CourseFilterBuilder
- */
- public Builder setDays(List days) {
- if (days != null) {
- RegexFilter.Builder builder = new RegexFilter.Builder();
- // + "{1}$" ensures only one occurrence of string
- days.forEach((d) -> {
- d = d + "{1}$";
- builder.addString(d);
- });
- this.days = builder.build();
- }
- return this;
- }
-
- /**
- * Set start after time
- *
- * @param startAfter Earliest a class can start / time class must start after
- * @return CourseFilterBuilder
- */
- public Builder setStartAfter(String startAfter) {
- try {
- if (startAfter != null)
- this.startAfter = new SimpleTime(startAfter);
- } catch (ParseException ignored) {
- LOGGER.error(new MessageBuilder(MessageBuilder.Type.COURSE).addDetails("Failed to Parse time string '%s'".formatted(startAfter)));
- }
- return this;
- }
-
- /**
- * Set end before time
- *
- * @param endBefore Latest a class can end / time class must end before
- * @return CourseFilterBuilder
- */
- public Builder setEndBefore(String endBefore) {
- try {
- if (endBefore != null)
- this.endAfter = new SimpleTime(endBefore);
- } catch (ParseException ignored) {
- LOGGER.error(new MessageBuilder(MessageBuilder.Type.COURSE).addDetails("Failed to Parse time string '%s'".formatted(startAfter)));
- }
- return this;
- }
-
- /**
- * Set online preference
- *
- * @param online Boolean string to include online classes
- * @return CourseFilterBuilder
- */
- public Builder setOnline(String online) {
- if (online != null)
- this.online = Boolean.parseBoolean(online) ? 1 : 0;
-
- return this;
- }
-
- /**
- * Set synchronous preference
- *
- * @param sync Boolean string to indicate synchronous preference. 1 syn, 0 sync, default both
- * @return CourseFilterBuilder
- */
- public Builder setSynchronous(String sync) {
- if (sync != null)
- this.synchronous = Boolean.parseBoolean(sync) ? 1 : 0;
-
- return this;
- }
-
-
- /**
- * Set list of instructors to search for
- *
- * @param instructors list of instructors to search for
- * @return CourseFilterBuilder
- */
- public Builder setInstructors(List instructors) {
- if (instructors != null) {
- RegexFilter.Builder builder = new RegexFilter.Builder();
- instructors.forEach(builder::addString);
- this.instructors = builder.build();
- }
- return this;
- }
-
- /**
- * Set list of keywords to search for in the Course titles
- *
- * @param keywords list of keywords to search for
- * @return CourseFilterBuilder
- */
- public Builder setKeywords(List keywords) {
- if (keywords != null) {
- RegexFilter.Builder builder = new RegexFilter.Builder();
- keywords.forEach(builder::addString);
- this.keywords = builder.build();
- }
- return this;
- }
-
- /**
- * Build Course filter
- *
- * @return Course Filter
- */
- public CourseFilter build() {
- return new CourseFilter(crns, codes, subjects, days, startAfter, endAfter, online, synchronous, instructors, keywords);
- }
+ public Set getSubjects(){
+ return this.subjects;
}
}
diff --git a/src/main/java/com/uh/rainbow/util/logging/MessageBuilder.java b/src/main/java/com/uh/rainbow/util/logging/MessageBuilder.java
index a54dc22..14aad84 100644
--- a/src/main/java/com/uh/rainbow/util/logging/MessageBuilder.java
+++ b/src/main/java/com/uh/rainbow/util/logging/MessageBuilder.java
@@ -22,7 +22,8 @@ public enum Type {
INST,
TERM,
SUBJECT,
- COURSE
+ COURSE,
+ SCHEDULE
}
private static final String DELIMITER = " | ";
diff --git a/src/test/java/com/uh/rainbow/entities/MeetingTest.java b/src/test/java/com/uh/rainbow/entities/MeetingTest.java
index b818273..ef48cdf 100644
--- a/src/test/java/com/uh/rainbow/entities/MeetingTest.java
+++ b/src/test/java/com/uh/rainbow/entities/MeetingTest.java
@@ -1,6 +1,5 @@
package com.uh.rainbow.entities;
-import com.uh.rainbow.dto.meeting.MeetingDTO;
import org.junit.jupiter.api.Test;
import java.text.ParseException;
@@ -18,7 +17,7 @@
public class MeetingTest {
@Test
- public void create_valid_meeting_with_simple_day(){
+ public void create_valid_meeting_with_simple_day() {
// Given
String dayString = "M";
String timeString = "0900-1030a";
@@ -32,13 +31,13 @@ public void create_valid_meeting_with_simple_day(){
// Then
assertEquals(1, meetings.size());
assertEquals(Day.MONDAY.getDow(), meetings.get(0).getDow());
- } catch (ParseException e){
+ } catch (ParseException e) {
fail(e);
}
}
@Test
- public void create_valid_meeting_with_complex_day(){
+ public void create_valid_meeting_with_complex_day() {
// Given
String dayString = "MW";
String timeString = "0900-1030a";
@@ -53,128 +52,132 @@ public void create_valid_meeting_with_complex_day(){
assertEquals(2, meetings.size());
assertEquals(Day.MONDAY.getDow(), meetings.get(0).getDow());
assertEquals(Day.WEDNESDAY.getDow(), meetings.get(1).getDow());
- } catch (ParseException e){
+ } catch (ParseException e) {
fail(e);
}
}
@Test
- public void a_start_before_and_end_before_b_start_and_end(){
+ public void a_start_before_and_end_before_b_start_and_end() {
/*
[ a ]
[ b ]
*/
- try{
+ try {
// Given
- Meeting a = new Meeting(new MeetingDTO("M", "foo", "09:00 am", "10:00 am", "10/1", "10/30"));
- Meeting b = new Meeting(new MeetingDTO("M", "foo", "11:00 am", "12:00 pm", "10/1", "10/30"));
+ Meeting a = Meeting.createMeetings("M", "0900-1000a", "foo", "10/1-10/30").get(0);
+ Meeting b = Meeting.createMeetings("M", "1100-1200p", "foo", "10/1-10/30").get(0);
// When
// Then
assertFalse(a.conflictsWith(b));
- } catch (ParseException e){
+ } catch (ParseException e) {
fail(e);
}
}
+
@Test
- public void a_end_after_b_start(){
+ public void a_end_after_b_start() {
/*
[ a ]
[ b ]
*/
- try{
+ try {
// Given
- Meeting a = new Meeting(new MeetingDTO("M", "foo", "09:00 am", "10:00 am", "10/1", "10/30"));
- Meeting b = new Meeting(new MeetingDTO("M", "foo", "9:30 am", "12:00 pm", "10/1", "10/30"));
+ Meeting a = Meeting.createMeetings("M", "0900-1000a", "foo", "10/1-10/30").get(0);
+ Meeting b = Meeting.createMeetings("M", "0930-1200p", "foo", "10/1-10/30").get(0);
// When
// Then
assertTrue(a.conflictsWith(b));
- } catch (ParseException e){
+ } catch (ParseException e) {
fail(e);
}
}
+
@Test
- public void a_start_before_b_start_and_end_after_b_end(){
+ public void a_start_before_b_start_and_end_after_b_end() {
/*
[ a ]
[ b ]
*/
- try{
+ try {
// Given
- Meeting a = new Meeting(new MeetingDTO("M", "foo", "09:00 am", "01:00 pm", "10/1", "10/30"));
- Meeting b = new Meeting(new MeetingDTO("M", "foo", "11:00 am", "12:00 pm", "10/1", "10/30"));
+ Meeting a = Meeting.createMeetings("M", "0900-0100p", "foo", "10/1-10/30").get(0);
+ Meeting b = Meeting.createMeetings("M", "1100-1200p", "foo", "10/1-10/30").get(0);
// When
// Then
assertTrue(a.conflictsWith(b));
- } catch (ParseException e){
+ } catch (ParseException e) {
fail(e);
}
}
+
@Test
- public void a_start_before_b_end(){
+ public void a_start_before_b_end() {
/*
[ a ]
[ b ]
*/
- try{
+ try {
// Given
- Meeting a = new Meeting(new MeetingDTO("M", "foo", "11:30 am", "01:00 pm", "10/1", "10/30"));
- Meeting b = new Meeting(new MeetingDTO("M", "foo", "11:00 am", "12:00 pm", "10/1", "10/30"));
+ Meeting a = Meeting.createMeetings("M", "1130-0100p", "foo", "10/1-10/30").get(0);
+ Meeting b = Meeting.createMeetings("M", "1100-1200p", "foo", "10/1-10/30").get(0);
// When
// Then
assertTrue(a.conflictsWith(b));
- } catch (ParseException e){
+ } catch (ParseException e) {
fail(e);
}
}
+
@Test
- public void a_start_before_and_end_after_b_start(){
+ public void a_start_before_and_end_after_b_start() {
/*
[ a ]
[ b ]
*/
- try{
+ try {
// Given
- Meeting a = new Meeting(new MeetingDTO("M", "foo", "12:30 pm", "01:00 pm", "10/1", "10/30"));
- Meeting b = new Meeting(new MeetingDTO("M", "foo", "11:00 am", "12:00 pm", "10/1", "10/30"));
+ Meeting a = Meeting.createMeetings("M", "1230-0100p", "foo", "10/1-10/30").get(0);
+ Meeting b = Meeting.createMeetings("M", "1100-1200p", "foo", "10/1-10/30").get(0);
// When
// Then
assertFalse(a.conflictsWith(b));
- } catch (ParseException e){
+ } catch (ParseException e) {
fail(e);
}
}
@Test
- public void a_overlap_with_b_on_different_days(){
- try{
+ public void a_overlap_with_b_on_different_days() {
+ try {
// Given
- Meeting a = new Meeting(new MeetingDTO("M", "foo", "11:00 am", "12:00 pm", "10/1", "10/30"));
- Meeting b = new Meeting(new MeetingDTO("T", "foo", "11:00 am", "12:00 pm", "10/1", "10/30"));
+ Meeting a = Meeting.createMeetings("M", "1100-1200p", "foo", "10/1-10/30").get(0);
+ Meeting b = Meeting.createMeetings("T", "1100-1200p", "foo", "10/1-10/30").get(0);
// When
// Then
assertFalse(a.conflictsWith(b));
- } catch (ParseException e){
+ } catch (ParseException e) {
fail(e);
}
}