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