diff --git a/inc/class.course.inc b/inc/class.course.inc --- a/inc/class.course.inc +++ b/inc/class.course.inc @@ -1,6 +1,6 @@ . */ -include_once 'class.section.php'; +require_once 'class.section.php'; +require_once 'class.course_slot.inc'; /** * \brief @@ -28,13 +29,22 @@ include_once 'class.section.php'; * course, a student has to choose a particular Section to * take. Courses are not associated with professors or meeting times, * those are in the realm of the Section and SectionMeeting. + * + * Iterating over this object will return CourseSlot objects, which + * act exactly like most universities' idea of a course. However, some + * universities have one Course and require students to take one + * section from each of different categories of sections within in a + * course. For example, at umich for any course which has a listing of + * Sections of the type 'discussion' the student _must_ take one of + * these 'discussion' sections in addition to, for example, a + * 'lecture' Section. */ class Course implements IteratorAggregate { private $name; // String private $title; // String - private $sections; // Array of sections - private $nsections; // int + private $sections; // Array of sections, for __wakeup() to convert to CourseSlots. + private $nsections; // int, for __wakeup() to convert to CourseSlots. /** * \brief * Other courses that this course depends on. @@ -46,6 +56,11 @@ class Course implements IteratorAggregat /** * \brief * Creates a class with the given name. + * + * When updating this function, update the call to ``new Course()'' + * in Schedule::findPossibilities(), Schedule::writeoutTables(), and + * Schedule::courses_get(). + * * \param $course_id * The identifier of the class. Ex., MATH-101 in * MATH-101-A. Retrieved with Course::getName(). @@ -53,23 +68,45 @@ class Course implements IteratorAggregat * The human-friendly course title, such as 'Introduction to * Algebra', or NULL. */ - function __construct($course_id, $title = NULL) + public function __construct($course_id, $title = NULL) { - $this->sections = array(); + $this->slots = array(); $this->name = $course_id; $this->title = $title; - $this->nsections = 0; $this->dependencies = array(); } /** * \brief - * Adds an already-instantiated section to this class. + * Adds an already-instantiated Section to this class. + * + * \param $section + * The Section to append to this Course. + * \param $course_slot_id + * The string identifer of the CourseSlot to place the given + * Section into. Most schools will not specify this. */ - public function section_add(Section $section) + public function section_add(Section $section, $course_slot_id = 'default') { - $this->sections[$this->nsections] = $section; - $this->nsections++; + if (empty($this->slots[$course_slot_id])) + $this->slots[$course_slot_id] = new CourseSlot($course_slot_id); + $this->slots[$course_slot_id]->section_add($section); + } + + /** + * \brief + * Append a CourseSlot to this Course. + * + * If this course already contains a CourseSlot with the same + * CourseSlot identifier as $course_slot, then the new CourseSlot + * will replace the old one. + * + * \param $course_slot + * The CourseSlot to append. + */ + public function course_slot_add(CourseSlot $course_slot) + { + $this->slots[$course_slot->id_get()] = $course_slot; } /** @@ -78,37 +115,17 @@ class Course implements IteratorAggregat */ public function getIterator() { - return new ArrayIterator($this->sections); + return new ArrayIterator($this->slots); } /** * \brief - * Returns the number of sections in the class. - */ - function getnsections() - { - return $this->nsections; - } - - /** - * \brief - * Returns the desired section for analysis. - * \return - * The selected section of the course. - */ - function getSection($i) - { - $result = $this->sections[$i]; - return $result; - } - - /** - * \brief - * Retrieve a section of this class based on its letter. + * Retrieve a section of this Course based on its letter. * - * \todo Make this function replace completely the getSection() - * function, have $this->sections be keyed by letter, and have a - * __wakup() convert the old $this->sections format to the new one. + * This function will automatically search CourseSlots for this + * Section. Why? Because even though we support multiple + * CourseSlots, the section_id must _still_ be unique -- no two + * CourseSlots should share a fully-qualified section_id. * * \return * The requested section or NULL if that section does not yet @@ -116,19 +133,20 @@ class Course implements IteratorAggregat */ public function section_get($letter) { - foreach ($this->sections as $section) { - if ($section->getLetter() == $letter) { - return $section; + foreach ($this->slots as $slot) + { + $section = $slot->section_get($letter); + if (!empty($section)) + return $section; } - } return NULL; } /** * \brief - * Returns the name of the class. + * Returns the name of the Course. * \return - * The name of the class. + * The name of the Course. */ public function getName() { @@ -180,16 +198,15 @@ class Course implements IteratorAggregat public static function parse($course_spec) { $section_parts = Section::parse($course_spec); - if (isset($section_parts['section'])) { + if (isset($section_parts['section'])) unset($section_parts['section']); - } return $section_parts; } /** * \brief - * Represent this class as a string. + * Represent this Course as a string. */ public function __toString() { @@ -215,12 +232,9 @@ class Course implements IteratorAggregat 'sections' => array(), 'dependencies' => array(), ); - foreach ($this->sections as $section) - { - $section_json_arrays = $section->to_json_arrays(); - foreach ($section_json_arrays as $section_json_array) - $json_array['sections'][] = $section_json_array; - } + foreach ($this->slots as $slot) + foreach ($slot->to_json_arrays() as $slot_json_section_array) + $json_array['sections'][] = $slot_json_section_array; foreach ($this->dependencies as $dependency) { @@ -255,8 +269,23 @@ class Course implements IteratorAggregat $course = new Course($json['course'], $title); if (!empty($json['sections'])) - $course->section_add(Section::from_json_arrays($json['sections'])); - + { + $json_course_slot_sections = array(); + foreach ($json['sections'] as $json_section) + { + $slot_id = 'default'; + if (!empty($json_section['slot'])) + $slot_id = $json_section['slot']; + + if (empty($json_course_slot_sections[$slot_id])) + $json_course_slot_sections[$slot_id] = array(); + $json_course_slot_sections[$slot_id][] = $json_section; + } + + foreach ($json_course_slot_sections as $slot_id => $json_course_slot_section) + $course->section_add(Section::from_json_arrays($json_course_slot_section), $slot_id); + } + if (!empty($json['dependencies'])) foreach ($json['dependencies'] as $dependency) $course->dependency_add(Course::from_json_array($dependency)); @@ -275,5 +304,16 @@ class Course implements IteratorAggregat if (!isset($this->title)) $this->title = NULL; + + if (empty($this->slots) && !empty($this->sections)) + { + $this->slots = array(); + + foreach ($this->sections as $section) + $this->section_add($section); + + unset($this->sections); + unset($this->nsections); + } } } diff --git a/inc/class.course_slot.inc b/inc/class.course_slot.inc new file mode 100644 --- /dev/null +++ b/inc/class.course_slot.inc @@ -0,0 +1,165 @@ +. + */ + +require_once 'class.section.php'; + +/** + * \brief + * A package of Section objects of which one must be taken to sign + * up for a particular Course. + * + * For example, some schools like umich have a single Course where one + * must sign up for one Section for every meeting_type of the + * following: 'lecture', 'discussion', and 'lab'. This way they avoid + * creating separate Course objects for labs (which is calvin's + * solution to the problem). + * + * Many schools do not have the CourseSlot paradigm. These will work + * just fine using one default CourseSlot. + * + * Iterating over this object will yield Section objects. + */ +class CourseSlot implements IteratorAggregate +{ + /** + * \brief + * An array of Section objects associated with this CourseSlot. + */ + private $sections; + + /** + * \brief + * An identifier for this slot. Used during crawling when sorting + * Sections into CourseSlot objects. + */ + private $id; + + /** + * \brief + * Creates a CourseSlot with the given identifier. + */ + public function __construct($id) + { + $this->id = $id; + } + + /** + * \brief + * Required function to implement the IteratorAggregate interface. + */ + public function getIterator() + { + return new ArrayIterator($this->sections); + } + + /** + * \brief + * Get the identifier of this slot. + * + * \return + * The slot's identifier string. + */ + public function id_get() + { + return $this->id; + } + + /** + * \brief + * Appends a Section to this CourseSlot. + * + * \param $section + * The Section to append. + */ + public function section_add(Section $section) + { + /* + * This behavior of the Schedule class requires this manner of + * indexing sections because it iterates using for ($count = 0; + * $count < ...) -style loops. Thus we allow PHP's natural + * indexing mechanism to do its job... + */ + $this->sections[] = $section; + } + + /** + * \brief + * Returns the number of sections in the class. + */ + function sections_count() + { + return count($this->sections); + } + + /** + * \brief + * Returns the desired section for analysis. + * \return + * The selected section of the course. + */ + function section_get_i($i) + { + $result = $this->sections[$i]; + return $result; + } + + /** + * \brief + * Retrieve a section of this class based on its letter. + * + * \todo Make this function replace completely the getSection() + * function, have $this->sections be keyed by letter, and have a + * __wakup() convert the old $this->sections format to the new one. + * + * \return + * The requested section or NULL if that section does not yet + * exist for this class. + */ + public function section_get($letter) + { + foreach ($this->sections as $section) { + if ($section->getLetter() == $letter) { + return $section; + } + } + return NULL; + } + + /** + * \brief + * Get the JSON arrays of data specific to each Section, adding + * slight metadata for this SLOT. + * + * There is no corresponding from_json_arrays() function for this + * class. See Course::from_json_array() which manages the conversion + * of JSON slots to CourseSlot objects. + */ + public function to_json_arrays() + { + $slot_section_json_arrays = array(); + foreach ($this->sections as $section) + { + $section_json_arrays = $section->to_json_arrays(); + foreach ($section_json_arrays as $section_json_array) + $slot_section_json_arrays[] = $section_json_array + array('slot' => $this->id); + } + return $slot_section_json_arrays; + } +} diff --git a/inc/class.schedule.php b/inc/class.schedule.php --- a/inc/class.schedule.php +++ b/inc/class.schedule.php @@ -47,6 +47,21 @@ class Schedule private $classStorage; // array of courses private $nclasses; // Integer number of classes + /** + * \brief + * Provides a mapping to regain the user's original input. + * + * Currently, the Schedule object cannot natively handle CourseSlot + * objects properly. It assumes that each Course has one and only + * one CourseSlot. This array maps each Course object stored in + * $classStorage onto the index of the course it was originally + * from. I.e., if the Course at index 0 had two CourseSlot objects, + * array(0 => 0, 1 => 0, 2 => 1) would map these two CourseSlot + * objects onto the same Course object and have the next CourseSlot + * be mapped into a separate Course object. + */ + private $course_slot_mappings; + /* My member variables. */ private $courses; private $nPermutations = 0; // Integer number of real permutations @@ -111,6 +126,7 @@ class Schedule function __construct($name, $parent = NULL, array $school = NULL, array $semester = NULL) { $this->courses = array(); + $this->course_slot_mappings = array(); $this->scheduleName = $name; $this->storage = array(); $this->title = "SlatePermutate - Scheduler"; @@ -141,6 +157,12 @@ class Schedule /** * \brief * Adds a new class to the schedule. + * + * \param $slot + * Currently, the Schedule class is not smart enough to understand + * CourseSlots. At a lower level, we split Courses with multiple + * CourseSlots into multiple Course objects with redundant + * information. */ function addCourse($course_id, $title) { @@ -158,10 +180,11 @@ class Schedule * NULL on success, a string on error which is a message for the * user and a valid XHTML fragment. */ - function addSection($course_name, $letter, $time_start, $time_end, $days, $synonym = NULL, $instructor = NULL, $location = NULL, $type = 'lecture') + function addSection($course_name, $letter, $time_start, $time_end, $days, $synonym = NULL, $instructor = NULL, $location = NULL, $type = 'lecture', $slot = 'default') { if (empty($letter) && (empty($time_start) || !strcmp($time_start, 'none')) && (empty($time_end) || !strcmp($time_end, 'none')) && empty($days) - && empty($synonym) && empty($instructor) && empty($location) && (empty($type) || !strcmp($type, 'lecture'))) + && empty($synonym) && empty($instructor) && empty($location) && (empty($type) || !strcmp($type, 'lecture')) + && (empty($slot) || !strcmp($slot, 'default'))) return; /* reject invalid times */ @@ -179,7 +202,7 @@ class Schedule if (!$section) { $section = new Section($letter, array(), $synonym); - $course->section_add($section); + $course->section_add($section, $slot); } $section->meeting_add(new SectionMeeting($days, $time_start, $time_end, $location, $type, $instructor)); @@ -236,17 +259,42 @@ class Schedule function findPossibilities() { /* + * Split out any Course objects with multiple CourseSlots + * into multiple Course objects... + */ + $new_courses = array(); + foreach ($this->courses as $i => $course) + foreach ($course as $course_slot) + { + $new_course = new Course($course->getName(), $course->title_get()); + $new_course->course_slot_add($course_slot); + $new_courses[] = $new_course; + + $this->course_slot_mappings[count($new_courses) - 1] = $i; + } + $this->courses = $new_courses; + unset($new_courses); + + /* * Clean crud (completely empty courses) out of the * schedule. For some crud, it's much easier to detect that * it's crud now than during parsing of postData[]. + * + * Now we may assume that each Course only has one + * CourseSlot... */ foreach ($this->courses as $i => $course) - if (!$course->getnsections()) - { - unset($this->courses[$i]); - $this->courses = array_values($this->courses); - return $this->findPossibilities(); - } + { + $course_slot = NULL; + foreach ($course as $course_slot) + break; + if (empty($course_slot) || !$course_slot->sections_count()) + { + unset($this->courses[$i]); + $this->courses = array_values($this->courses); + return $this->findPossibilities(); + } + } $this->possiblePermutations = 1; /* special case: there is nothing entered into the schedule and thus there is one, NULL permutation */ @@ -263,7 +311,15 @@ class Schedule $i = 0; foreach ($this->courses as $course) { - $this->possiblePermutations = $this->possiblePermutations * $course->getnsections(); + /* + * Kludge for until we support course_slots natively + * or find a better solution. + */ + unset($course_slot); + foreach ($course as $course_slot) + break; + + $this->possiblePermutations = $this->possiblePermutations * $course_slot->sections_count(); $cs[$i] = 0; // Sets the counter array to all zeroes. $i ++; } @@ -279,15 +335,22 @@ class Schedule for ($downCounter = count($this->courses) - 1; $downCounter > $upCounter && !$conflict; $downCounter --) { - if ($this->courses[$upCounter]->getSection($cs[$upCounter]) - ->conflictsWith($this->courses[$downCounter]->getSection($cs[$downCounter]))) + unset($course_slot_up); + foreach ($this->courses[$upCounter] as $course_slot_up) + break; + + unset($course_slot_down); + foreach ($this->courses[$downCounter] as $course_slot_down) + break; + + if ($course_slot_up->section_get_i($cs[$upCounter])->conflictsWith($course_slot_down->section_get_i($cs[$downCounter]))) { $conflict = TRUE; break; } } } - + // Store to storage if no conflict is found. if(!$conflict) { @@ -297,15 +360,19 @@ class Schedule } $this->nPermutations++; } - + // Increase the counter by one to get the next combination of class sections. $cs[$position] = $cs[$position] + 1; - + // Check to make sure the counter is still valid. $valid = false; while(!$valid) { - if($cs[$position] == $this->courses[$position]->getnsections()) + unset($course_slot); + foreach ($this->courses[$position] as $course_slot) + break; + + if($cs[$position] == $course_slot->sections_count()) { $cs[$position] = 0; @@ -330,7 +397,7 @@ class Schedule $counter++; } while($counter < $this->possiblePermutations); } - + /** * \brief * Prints out the possible permutations in tables. @@ -458,9 +525,10 @@ class Schedule $min_time = (int)min($time); $sort_time = FALSE; foreach ($this->courses as $course) + foreach ($course as $course_slot) { - for ($si = 0; $si < $course->getnsections(); $si ++) - foreach ($course->getSection($si)->getMeetings() as $meeting) + for ($si = 0; $si < $course_slot->sections_count(); $si ++) + foreach ($course_slot->section_get_i($si)->getMeetings() as $meeting) { /* Saturdayness */ if ($meeting->getDay(5)) @@ -589,10 +657,13 @@ class Schedule for($j = 0; $j < count($this->courses); $j++) { $course = $this->courses[$j]; - $section_index = $this->storage[$i][$j]; - $section = $course->getSection($section_index); - /* iterate through all of a class's meeting times */ - $meetings = $section->getMeetings(); + foreach ($course as $course_slot) + { + $section_index = $this->storage[$i][$j]; + $section = $course_slot->section_get_i($section_index); + + /* iterate through all of a class's meeting times */ + $meetings = $section->getMeetings(); /* find any meeting which are going on at this time */ $current_meeting = NULL; @@ -644,12 +715,13 @@ class Schedule if (empty($permutations_courses[$j])) { $singleton_course = new Course($course->getName(), $course->title_get()); - $singleton_course->section_add($section); + $singleton_course->section_add($section, $course_slot->id_get()); $permutation_courses[$j] = $singleton_course->to_json_array(); } $filled = TRUE; } + } /* $course_slot */ } } @@ -723,15 +795,46 @@ class Schedule /** * \brief - * fetch a specified class by its key + * Fetch a specified class by its key. + * + * Use Schedule::courses_get() instead of this function if the code + * you're writing understand CourseSlot objects. + * + * \see Schedule::courses_get(). */ - function class_get($class_key) + public function class_get($class_key) { return $this->courses[$class_key]; } /** * \brief + * Get an array of Course objects as originally inputted by the + * user. + */ + public function courses_get() + { + /* + * As Mr. Westra would say, just map them courses back into their + * original forms. + */ + + $courses = array(); + foreach ($this->courses as $course_i => $course) + { + $mapping = $this->course_slot_mappings[$course_i]; + if (empty($courses[$mapping])) + $courses[$mapping] = new Course($course->getName(), $course->title_get()); + + foreach ($course as $course_slot) + $courses[$mapping]->course_slot_add($course_slot); + } + + return $courses; + } + + /** + * \brief * Set my global ID. * * Only to be called by schedule_store_store(). @@ -804,16 +907,15 @@ class Schedule */ function __wakeup() { - if ($this->nclasses == -1) - /* this Schedule doesn't need to be upgraded from Classes to Course */ - return; + if ($this->nclasses != -1) + { + /* this Schedule needs to be upgraded from Classes to Course */ - $this->courses = array(); - foreach ($this->classStorage as $classes) - { - $this->courses[] = $classes->to_course(); + $this->courses = array(); + foreach ($this->classStorage as $classes) + $this->courses[] = $classes->to_course(); + $this->nclasses = -1; } - $this->nclasses = -1; if (empty($this->parent_id)) $this->parent_id = NULL; @@ -836,5 +938,12 @@ class Schedule $this->semester = school_semester_guess($school); } } + + if (empty($this->course_slot_mappings)) + { + $this->course_slot_mappings = array(); + foreach ($this->courses as $course_i => $course) + $this->course_slot_mappings[$course_i] = count($this->course_slot_mappings); + } } } diff --git a/inc/class.semester.inc b/inc/class.semester.inc --- a/inc/class.semester.inc +++ b/inc/class.semester.inc @@ -155,8 +155,14 @@ class Semester * The section itself. * \param $title * The course human-friendly title. + * \param $course_slot_id + * The slot of the course which this section should be added + * to. Use 'default' (or don't pass this parameter) if your school + * does not have the concept of course slots. Ask binki for help + * figuring this out. Course slots are a sort of + * inverse/complement to section_meetings. */ - public function section_add($dept, $class, Section $section, $title = NULL) + public function section_add($dept, $class, Section $section, $title = NULL, $course_slot_id = 'default') { $dept = strtoupper($dept); $class = strtoupper($class); @@ -172,7 +178,7 @@ class Semester $classobj = $this->departments[$dept][$class]; } - $classobj->section_add($section); + $classobj->section_add($section, $course_slot_id); } /** @@ -198,8 +204,11 @@ class Semester * \param $section_meeting * The SectionMeeting to be added to a section which may or may * not already be in this Semester. + * \param $course_slot_id + * The name of the new CourseSlot to create if the given section + * does not yet exist. */ - public function section_meeting_add($dept, $course, $title, $section, $synonym, $section_meeting) + public function section_meeting_add($dept, $course, $title, $section, $synonym, $section_meeting, $course_slot_id = 'default') { $dept = strtoupper($dept); $course = strtoupper($course); @@ -212,7 +221,7 @@ class Semester $section_obj = $course_obj->section_get($section); } if (empty($course_obj) || empty($section_obj)) - return $this->section_add($dept, $course, new Section($section, array($section_meeting), $synonym), $title); + return $this->section_add($dept, $course, new Section($section, array($section_meeting), $synonym), $title, $course_slot_id); $section_obj->meeting_add($section_meeting); return; diff --git a/input.php b/input.php --- a/input.php +++ b/input.php @@ -125,10 +125,9 @@ jQuery(document).ready( '; if ($sch) { - $nclasses = $sch->nclasses_get(); - for ($class_key = 0; $class_key < $nclasses; $class_key ++) + foreach ($sch->courses_get() as $course) { - $my_hc .= input_class_js($sch->class_get($class_key), ' '); + $my_hc .= input_course_js($course, ' '); } } elseif ($errors_fix) @@ -153,7 +152,8 @@ elseif ($errors_fix) 's' => !empty($section['days'][5]))) . ', ' . json_encode($section['professor']) . ', ' . json_encode($section['location']) . ', ' - . json_encode($section['type']) . ');' . PHP_EOL; + . json_encode($section['type']) . ', ' + . json_encode($section['slot']) . ');' . PHP_EOL; $my_hc .= PHP_EOL; } } @@ -161,7 +161,7 @@ else { $default_courses = school_default_courses($school); foreach ($default_courses as $default_class) - $my_hc .= input_class_js($default_class, ' '); + $my_hc .= input_course_js($default_class, ' '); } $my_hc .= ' class_last = add_class();' . PHP_EOL; if ($qtips_always || !isset($_SESSION['saw_qtips'])) @@ -330,7 +330,7 @@ if (!empty($_REQUEST['selectsemester'])) $inputPage->showSchoolInstructions(); $inputPage->foot(); -function input_class_js(Course $course, $whitespace = ' ') +function input_course_js(Course $course, $whitespace = ' ') { $title = $course->title_get(); if (empty($title)) @@ -338,11 +338,10 @@ function input_class_js(Course $course, $js = $whitespace . 'class_last = add_class_n(' . json_encode($course->getName()) . ', ' . json_encode($title) . ');' . PHP_EOL; - $nsections = $course->getnsections(); - for ($section_key = $nsections - 1; $section_key >= 0; $section_key --) - { - $section = $course->getSection($section_key); - $meetings = $section->getMeetings(); + foreach ($course as $course_slot) + foreach ($course_slot as $section) + { + $meetings = $section->getMeetings(); foreach ($meetings as $meeting) { $js .= $whitespace . 'add_section_n(class_last, ' . json_encode($section->getLetter()) . ', ' @@ -352,8 +351,9 @@ function input_class_js(Course $course, . json_encode(array('m' => $meeting->getDay(0), 't' => $meeting->getDay(1), 'w' => $meeting->getDay(2), 'h' => $meeting->getDay(3), 'f' => $meeting->getDay(4), 's' => $meeting->getDay(5))) . ', ' . json_encode($meeting->instructor_get()) . ', ' - . json_encode($meeting->getLocation()) . ',' - . json_encode($meeting->type_get()) . ');' . PHP_EOL; + . json_encode($meeting->getLocation()) . ', ' + . json_encode($meeting->type_get()) . ', ' + . json_encode($course_slot->id_get()) . ');' . PHP_EOL; } } diff --git a/process.php b/process.php --- a/process.php +++ b/process.php @@ -208,7 +208,10 @@ if(!$DEBUG) /* Skip the section name, which isn't a section */ if(is_array($section)) { - $error_string = $allClasses->addSection($course['name'], $section['letter'], $section['start'], $section['end'], arrayToDays(empty($section['days']) ? array() : $section['days'], 'alpha'), $section['synonym'], $section['professor'], $section['location'], $section['type']); + if (empty($section['slot'])) + $section['slot'] = 'default'; + + $error_string = $allClasses->addSection($course['name'], $section['letter'], $section['start'], $section['end'], arrayToDays(empty($section['days']) ? array() : $section['days'], 'alpha'), $section['synonym'], $section['professor'], $section['location'], $section['type'], $section['slot']); if ($error_string !== NULL) $errors[] = $error_string; } diff --git a/school.d/default.inc b/school.d/default.inc --- a/school.d/default.inc +++ b/school.d/default.inc @@ -82,8 +82,9 @@ function default_registration_html(Page . '
' . PHP_EOL . '