/// <summary> /// Remove the record that says a subject needs to run on a specific time /// </summary> public void UnForceTime(Subject subject) { if (SubjectsWithForcedTimes.Remove(subject)) { Order(); } }
/// <summary> /// Find what time a subject is forced to /// </summary> /// <returns>If the subject is not forced on any time, return null</returns> public Time?GetForcedTime(Subject subject) { if (SubjectsWithForcedTimes.Contains(subject)) { return(AssignedTimes[subject]); } return(null); }
/// <summary> /// Assign a time to all selected subjects /// </summary> private void Order() { Stopwatch timerOrder = new Stopwatch(); timerOrder.Restart(); // Check that all forcedTimes are allowed foreach (Subject subject in SubjectsWithForcedTimes) { if (!subject.AllowedDuringSemester(AssignedTimes[subject], this)) { SubjectsWithForcedTimes.Remove(subject); } } // Remove current time assignments foreach (Subject subject in SelectedSubjects) { if (!SubjectsWithForcedTimes.Contains(subject)) { AssignedTimes.Remove(subject); } } // This variable works because IEnumerables get evaluated every time a method is called on it. I usually use a local function instead of a variable. IEnumerable <Subject> RemainingSubjects = SelectedSubjects.Except(SelectedSubjectsSoFar(Time.All)).OrderBy(subject => subject.GetLevel()); for (Time semester = Time.First; MaxCreditPoints.Keys.Contains(semester) || RemainingSubjects.Any(); semester = semester.Next()) { // If neccessary, add another year to the planner if (!MaxCreditPoints.Keys.Contains(semester)) { AddYear(); } // Fill the semester with subjects that can be chosen int selectedCredits = SelectedSubjects.Where(subject => AssignedTimes.TryGetValue(subject, out Time selectedTime) && selectedTime == semester).Sum(subject => subject.CreditPoints()); while (selectedCredits < GetMaxCreditPoints(semester)) { // Prepare a list of what subjects could be chosen var possibleSubjects = RemainingSubjects; // Do not pick subjects with forced times later than the current session possibleSubjects = possibleSubjects.Where(subject => !(SubjectsWithForcedTimes.Contains(subject) && semester.IsEarlierThan(AssignedTimes[subject]))); // Pick from subjects that are allowed during this semester possibleSubjects = possibleSubjects.Where(subject => subject.AllowedDuringSemester(semester, this)); // Pick from subjects which would not result in Credit Overflow possibleSubjects = possibleSubjects.Where(subject => subject.CreditPoints() + selectedCredits <= GetMaxCreditPoints(semester)); // Check if any subjects are forced IEnumerable <Subject> forcedSubjects = possibleSubjects .Where(subject => SubjectsWithForcedTimes.Contains(subject) && AssignedTimes[subject].IsEarlierThanOrAtTheSameTime(semester)); // If any subjects are forced, only consider the forced subjects if (forcedSubjects.Any()) { possibleSubjects = forcedSubjects.OrderBy(subject => AssignedTimes[subject]); } // Otherwise, filter subjects according to whether their requisites are completed else { possibleSubjects = possibleSubjects.Where(subject => RequisitesHaveBeenSelected(subject, semester)); } // Favor subjects that have many other subjects relying on them possibleSubjects = possibleSubjects.OrderByDescending(subject => RemainingSubjects.Sum(other => IsAbove(parent: other, child: subject, out int size) ? size : 0)) // Favor subjects that cannot fit in many semesters .ThenBy(subject => subject.Semesters.Select(semester => semester.session).Distinct().Count()) // Favor lower level subjects .ThenBy(subject => subject.GetLevel()) // Favor subjects that don't require many electives as prerequisites .ThenBy(subject => subject.Prerequisites.SizeOfElective() + subject.Corequisites.SizeOfElective()) // Favor subjects that are requisites to their Degree (or any course) .ThenBy(subject => ContentRelations.Any(relation => relation.source is Course && relation.dest == subject) ? 0 : 1); // Pick the first item from that list Subject nextSubject = possibleSubjects.FirstOrDefault(); // If no subject was chosen, go to the next semester if (nextSubject == null) { break; } // Add the selected subject to this semester AssignedTimes[nextSubject] = semester; // Keep track of how many more times this loop can repeat selectedCredits += nextSubject.CreditPoints(); } } if (SelectedSubjects.Except(SelectedSubjectsSoFar(Time.All)).Any()) { throw new InvalidOperationException("Not all the subjects were added to the table"); } timerOrder.Stop(); Debug.WriteLine("Ordering Plan: " + timerOrder.ElapsedMilliseconds + "ms"); // Helper functions to determine whether a subject can be picked bool RequisitesHaveBeenSelected(Subject subject, Time time, Decision requisite = null) { // This function goes through a subject's requisites to make sure every SelectedSubject that is a requisite to this subject has already been added to the schedule // If no requisite has been specified, then this must have been called by outside this function if (requisite == null) { // Check if this is one of those subjects which don't have any actual requisites (eg no data or "permission by special approval") // If so, assign it an earliest position based on its level if (subject.Prerequisites.HasBeenCompleted() && subject.Corequisites.HasBeenCompleted()) { return(time.AsNumber() / 2 >= subject.GetLevel() - 1); } // Call this function again but with the prerequisites and corequisites return(RequisitesHaveBeenSelected(subject, time.Previous(), subject.Prerequisites) && RequisitesHaveBeenSelected(subject, time, subject.Corequisites)); } // If the requisit is met by the scheduled selected subjects, return true if (requisite.HasBeenCompleted(this, time)) { return(true); } // If the requisit is an elective, don't process it. Instead, compare the subject's level to the time if (requisite.IsElective()) { return(subject.GetLevel() <= time.Next().year); } // Iterate over all of the requisite's option // The requisite's selectionType doesn't matter, all that matters is whether it HasBeenCompleted. foreach (Option option in requisite.Options) { // If the option is a subject that needs to be picked, hasn't been picked, and must come before the current subject: return false if (option is Content content && !SelectedSubjectsSoFar(time).Contains(content) && OptionMustComeBeforeSubject(parent: subject, child: content as Subject)) { return(false); }
/// <summary> /// Record that a subject needs to run on a specific time /// </summary> public void ForceTime(Subject subject, Time time) { AssignedTimes[subject] = time; SubjectsWithForcedTimes.Add(subject); Order(); }