001package org.cpsolver.studentsct.model;
002
003import java.text.DecimalFormat;
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.Collections;
007import java.util.HashMap;
008import java.util.HashSet;
009import java.util.List;
010import java.util.Map;
011import java.util.Set;
012import java.util.TreeSet;
013
014import org.cpsolver.coursett.model.TimeLocation;
015import org.cpsolver.ifs.assignment.Assignment;
016import org.cpsolver.ifs.assignment.AssignmentComparator;
017import org.cpsolver.ifs.util.ToolBox;
018import org.cpsolver.studentsct.StudentSectioningModel;
019import org.cpsolver.studentsct.constraint.ConfigLimit;
020import org.cpsolver.studentsct.constraint.CourseLimit;
021import org.cpsolver.studentsct.constraint.LinkedSections;
022import org.cpsolver.studentsct.constraint.SectionLimit;
023import org.cpsolver.studentsct.reservation.Reservation;
024
025
026/**
027 * Representation of a request of a student for one or more course. A student
028 * requests one of the given courses, preferably the first one. <br>
029 * <br>
030 * 
031 * @version StudentSct 1.3 (Student Sectioning)<br>
032 *          Copyright (C) 2007 - 2014 Tomas Muller<br>
033 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
034 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
035 * <br>
036 *          This library is free software; you can redistribute it and/or modify
037 *          it under the terms of the GNU Lesser General Public License as
038 *          published by the Free Software Foundation; either version 3 of the
039 *          License, or (at your option) any later version. <br>
040 * <br>
041 *          This library is distributed in the hope that it will be useful, but
042 *          WITHOUT ANY WARRANTY; without even the implied warranty of
043 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
044 *          Lesser General Public License for more details. <br>
045 * <br>
046 *          You should have received a copy of the GNU Lesser General Public
047 *          License along with this library; if not see
048 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
049 */
050public class CourseRequest extends Request {
051    private static DecimalFormat sDF = new DecimalFormat("0.000");
052    private List<Course> iCourses = null;
053    private Set<Choice> iWaitlistedChoices = new HashSet<Choice>();
054    private Set<Choice> iSelectedChoices = new HashSet<Choice>();
055    private boolean iWaitlist = false;
056    private Long iTimeStamp = null;
057    private Double iCachedMinPenalty = null, iCachedMaxPenalty = null;
058    public static boolean sSameTimePrecise = false;
059    private Set<RequestGroup> iRequestGroups = new HashSet<RequestGroup>();
060
061    /**
062     * Constructor
063     * 
064     * @param id
065     *            request unique id
066     * @param priority
067     *            request priority
068     * @param alternative
069     *            true if the request is alternative (alternative request can be
070     *            assigned instead of a non-alternative course requests, if it
071     *            is left unassigned)
072     * @param student
073     *            appropriate student
074     * @param courses
075     *            list of requested courses (in the correct order -- first is
076     *            the requested course, second is the first alternative, etc.)
077     * @param waitlist
078     *            time stamp of the request if the student can be put on a wait-list (no alternative
079     *            course request will be given instead)
080     * @param timeStamp request time stamp
081     */
082    public CourseRequest(long id, int priority, boolean alternative, Student student, java.util.List<Course> courses,
083            boolean waitlist, Long timeStamp) {
084        super(id, priority, alternative, student);
085        iCourses = new ArrayList<Course>(courses);
086        for (Course course: iCourses)
087            course.getRequests().add(this);
088        iWaitlist = waitlist;
089        iTimeStamp = timeStamp;
090    }
091
092    /**
093     * List of requested courses (in the correct order -- first is the requested
094     * course, second is the first alternative, etc.)
095     * @return requested course offerings
096     */
097    public List<Course> getCourses() {
098        return iCourses;
099    }
100
101    /**
102     * Create enrollment for the given list of sections. The list of sections
103     * needs to be correct, i.e., a section for each subpart of a configuration
104     * of one of the requested courses.
105     * @param sections selected sections
106     * @param reservation selected reservation
107     * @return enrollment
108     */
109    public Enrollment createEnrollment(Set<? extends SctAssignment> sections, Reservation reservation) {
110        if (sections == null || sections.isEmpty())
111            return null;
112        Config config = ((Section) sections.iterator().next()).getSubpart().getConfig();
113        Course course = null;
114        for (Course c: iCourses) {
115            if (c.getOffering().getConfigs().contains(config)) {
116                course = c;
117                break;
118            }
119        }
120        return new Enrollment(this, iCourses.indexOf(course), course, config, sections, reservation);
121    }
122
123    /**
124     * Create enrollment for the given list of sections. The list of sections
125     * needs to be correct, i.e., a section for each subpart of a configuration
126     * of one of the requested courses.
127     * @param assignment current assignment (to guess the reservation)
128     * @param sections selected sections
129     * @return enrollment
130     */
131    public Enrollment createEnrollment(Assignment<Request, Enrollment> assignment, Set<? extends SctAssignment> sections) {
132        Enrollment ret = createEnrollment(sections, null);
133        ret.guessReservation(assignment, true);
134        return ret;
135        
136    }
137    
138    /**
139     * Maximal domain size (i.e., number of enrollments of a course request), -1 if there is no limit.
140     * @return maximal domain size, -1 if unlimited
141     */
142    protected int getMaxDomainSize() {
143        StudentSectioningModel model = (StudentSectioningModel) getModel();
144        return model == null ? -1 : model.getMaxDomainSize();
145    }
146    
147    /**
148     * Return all possible enrollments.
149     */
150    @Override
151    public List<Enrollment> computeEnrollments(Assignment<Request, Enrollment> assignment) {
152        List<Enrollment> ret = new ArrayList<Enrollment>();
153        int idx = 0;
154        for (Course course : iCourses) {
155            for (Config config : course.getOffering().getConfigs()) {
156                computeEnrollments(assignment, ret, idx, 0, course, config, new HashSet<Section>(), 0, false, false,
157                        false, false, getMaxDomainSize() <= 0 ? -1 : ret.size() + getMaxDomainSize());
158            }
159            idx++;
160        }
161        return ret;
162    }
163
164    /**
165     * Return a subset of all enrollments -- randomly select only up to
166     * limitEachConfig enrollments of each config.
167     * @param assignment current assignment
168     * @param limitEachConfig maximal number of enrollments in each configuration
169     * @return computed enrollments
170     */
171    public List<Enrollment> computeRandomEnrollments(Assignment<Request, Enrollment> assignment, int limitEachConfig) {
172        List<Enrollment> ret = new ArrayList<Enrollment>();
173        int idx = 0;
174        for (Course course : iCourses) {
175            for (Config config : course.getOffering().getConfigs()) {
176                computeEnrollments(assignment, ret, idx, 0, course, config, new HashSet<Section>(), 0, false, false,
177                        false, true, (limitEachConfig <= 0 ? limitEachConfig : ret.size() + limitEachConfig));
178            }
179            idx++;
180        }
181        return ret;
182    }
183
184    /**
185     * Return true if the both sets of sections contain sections of the same
186     * subparts, and each pair of sections of the same subpart is offered at the
187     * same time.
188     */
189    private boolean sameTimes(Set<Section> sections1, Set<Section> sections2) {
190        for (Section s1 : sections1) {
191            Section s2 = null;
192            for (Section s : sections2) {
193                if (s.getSubpart().equals(s1.getSubpart())) {
194                    s2 = s;
195                    break;
196                }
197            }
198            if (s2 == null)
199                return false;
200            if (!ToolBox.equals(s1.getTime(), s2.getTime()))
201                return false;
202        }
203        return true;
204    }
205
206    /**
207     * Recursive computation of enrollments
208     * 
209     * @param enrollments
210     *            list of enrollments to be returned
211     * @param priority
212     *            zero for the course, one for the first alternative, two for the second alternative
213     * @param penalty
214     *            penalty of the selected sections
215     * @param course
216     *            selected course
217     * @param config
218     *            selected configuration
219     * @param sections
220     *            sections selected so far
221     * @param idx
222     *            index of the subparts (a section of 0..idx-1 subparts has been
223     *            already selected)
224     * @param availableOnly
225     *            only use available sections
226     * @param skipSameTime
227     *            for each possible times, pick only one section
228     * @param selectedOnly
229     *            select only sections that are selected (
230     *            {@link CourseRequest#isSelected(Section)} is true)
231     * @param random
232     *            pick sections in a random order (useful when limit is used)
233     * @param limit
234     *            when above zero, limit the number of selected enrollments to
235     *            this limit
236     * @param ignoreDisabled
237     *            are sections that are disabled for student scheduling allowed to be used
238     * @param reservations
239     *            list of applicable reservations
240     */
241    private void computeEnrollments(Assignment<Request, Enrollment> assignment, Collection<Enrollment> enrollments, int priority, double penalty, Course course, Config config,
242            HashSet<Section> sections, int idx, boolean availableOnly, boolean skipSameTime, boolean selectedOnly,
243            boolean random, int limit) {
244        if (limit > 0 && enrollments.size() >= limit)
245            return;
246        if (idx == 0) { // run only once for each configuration
247            boolean canOverLimit = false;
248            if (availableOnly) {
249                for (Reservation r: getReservations(course)) {
250                    if (!r.canBatchAssignOverLimit()) continue;
251                    if (!r.getConfigs().isEmpty() && !r.getConfigs().contains(config)) continue;
252                    if (r.getReservedAvailableSpace(assignment, this) < getWeight()) continue;
253                    canOverLimit = true; break;
254                }
255            }
256            if (!canOverLimit) {
257                if (availableOnly && config.getLimit() >= 0 && ConfigLimit.getEnrollmentWeight(assignment, config, this) > config.getLimit())
258                    return;
259                if (availableOnly && course.getLimit() >= 0 && CourseLimit.getEnrollmentWeight(assignment, course, this) > course.getLimit())
260                    return;
261                if (config.getOffering().hasReservations()) {
262                    boolean hasReservation = false, hasConfigReservation = false, reservationMustBeUsed = false;
263                    for (Reservation r: getReservations(course)) {
264                        if (r.mustBeUsed()) reservationMustBeUsed = true;
265                        if (availableOnly && r.getReservedAvailableSpace(assignment, this) < getWeight()) continue;
266                        if (r.getConfigs().isEmpty()) {
267                            hasReservation = true;
268                        } else if (r.getConfigs().contains(config)) {
269                            hasReservation = true;
270                            hasConfigReservation = true;
271                        }
272                    }
273                    if (!hasConfigReservation && config.getTotalUnreservedSpace() < getWeight())
274                        return;
275                    if (!hasReservation && config.getOffering().getTotalUnreservedSpace() < getWeight())
276                        return;
277                    if (availableOnly && !hasReservation && config.getOffering().getUnreservedSpace(assignment, this) < getWeight())
278                        return;
279                    if (availableOnly && !hasConfigReservation && config.getUnreservedSpace(assignment, this) < getWeight())
280                        return;
281                    if (!hasReservation && reservationMustBeUsed)
282                        return;
283                }
284            }
285        }
286        if (config.getSubparts().size() == idx) {
287            if (skipSameTime && sSameTimePrecise) {
288                boolean waitListedOrSelected = false;
289                if (!getSelectedChoices().isEmpty() || !getWaitlistedChoices().isEmpty()) {
290                    for (Section section : sections) {
291                        if (isWaitlisted(section) || isSelected(section)) {
292                            waitListedOrSelected = true;
293                            break;
294                        }
295                    }
296                }
297                if (!waitListedOrSelected) {
298                    for (Enrollment enrollment : enrollments) {
299                        if (sameTimes(enrollment.getSections(), sections))
300                            return;
301                    }
302                }
303            }
304            if (!config.getOffering().hasReservations()) {
305                enrollments.add(new Enrollment(this, priority, null, config, new HashSet<SctAssignment>(sections), null));
306            } else {
307                Enrollment e = new Enrollment(this, priority, null, config, new HashSet<SctAssignment>(sections), null);
308                boolean mustHaveReservation = config.getOffering().getTotalUnreservedSpace() < getWeight();
309                boolean mustHaveConfigReservation = config.getTotalUnreservedSpace() < getWeight();
310                boolean mustHaveSectionReservation = false;
311                boolean containDisabledSection = false;
312                for (Section s: sections) {
313                    if (s.getTotalUnreservedSpace() < getWeight()) {
314                        mustHaveSectionReservation = true;
315                    }
316                    if (!getStudent().isAllowDisabled() && !s.isEnabled()) {
317                        containDisabledSection = true;
318                    }
319                }
320                boolean canOverLimit = false;
321                if (availableOnly) {
322                    for (Reservation r: getReservations(course)) {
323                        if (!r.canBatchAssignOverLimit() || !r.isIncluded(e)) continue;
324                        if (r.getReservedAvailableSpace(assignment, this) < getWeight()) continue;
325                        if (containDisabledSection && !r.isAllowDisabled()) continue;
326                        enrollments.add(new Enrollment(this, priority, null, config, new HashSet<SctAssignment>(sections), r));
327                        canOverLimit = true;
328                    }
329                }
330                if (!canOverLimit) {
331                    boolean reservationMustBeUsed = false;
332                    reservations: for (Reservation r: (availableOnly ? getSortedReservations(assignment, course) : getReservations(course))) {
333                        if (r.mustBeUsed()) reservationMustBeUsed = true;
334                        if (!r.isIncluded(e)) continue;
335                        if (availableOnly && r.getReservedAvailableSpace(assignment, this) < getWeight()) continue;
336                        if (mustHaveConfigReservation && r.getConfigs().isEmpty()) continue;
337                        if (mustHaveSectionReservation)
338                            for (Section s: sections)
339                                if (r.getSections(s.getSubpart()) == null && s.getTotalUnreservedSpace() < getWeight()) continue reservations;
340                        if (containDisabledSection && !r.isAllowDisabled()) continue;
341                        enrollments.add(new Enrollment(this, priority, null, config, new HashSet<SctAssignment>(sections), r));
342                        if (availableOnly) return; // only one available reservation suffice (the best matching one)
343                    }
344                    // a case w/o reservation
345                    if (!(mustHaveReservation || mustHaveConfigReservation || mustHaveSectionReservation) &&
346                        !(availableOnly && config.getOffering().getUnreservedSpace(assignment, this) < getWeight()) &&
347                        !reservationMustBeUsed && !containDisabledSection) {
348                        enrollments.add(new Enrollment(this, (getReservations(course).isEmpty() ? 0 : 1) + priority, null, config, new HashSet<SctAssignment>(sections), null));
349                    }
350                }
351            }
352        } else {
353            Subpart subpart = config.getSubparts().get(idx);
354            HashSet<TimeLocation> times = (skipSameTime ? new HashSet<TimeLocation>() : null);
355            List<Section> sectionsThisSubpart = subpart.getSections();
356            if (skipSameTime) {
357                sectionsThisSubpart = new ArrayList<Section>(subpart.getSections());
358                Collections.sort(sectionsThisSubpart, new AssignmentComparator<Section, Request, Enrollment>(assignment));
359            }
360            List<Section> matchingSectionsThisSubpart = new ArrayList<Section>(subpart.getSections().size());
361            boolean hasChildren = !subpart.getChildren().isEmpty();
362            for (Section section : sectionsThisSubpart) {
363                if (section.isCancelled())
364                    continue;
365                if (getInitialAssignment() != null && (getModel() != null && ((StudentSectioningModel)getModel()).getKeepInitialAssignments()) &&
366                        !getInitialAssignment().getAssignments().contains(section))
367                    continue;
368                if (section.getParent() != null && !sections.contains(section.getParent()))
369                    continue;
370                if (section.isOverlapping(sections))
371                    continue;
372                if (selectedOnly && !isSelected(section))
373                    continue;
374                if (!getStudent().isAvailable(section)) {
375                    boolean canOverlap = false;
376                    for (Reservation r: getReservations(course)) {
377                        if (!r.isAllowOverlap()) continue;
378                        if (r.getSections(subpart) != null && !r.getSections(subpart).contains(section)) continue;
379                        if (r.getReservedAvailableSpace(assignment, this) < getWeight()) continue;
380                        canOverlap = true; break;
381                    }
382                    if (!canOverlap) continue;
383                }
384                boolean canOverLimit = false;
385                if (availableOnly) {
386                    for (Reservation r: getReservations(course)) {
387                        if (!r.canBatchAssignOverLimit()) continue;
388                        if (r.getSections(subpart) != null && !r.getSections(subpart).contains(section)) continue;
389                        if (r.getReservedAvailableSpace(assignment, this) < getWeight()) continue;
390                        canOverLimit = true; break;
391                    }
392                }
393                if (!canOverLimit) {
394                    if (availableOnly && section.getLimit() >= 0
395                            && SectionLimit.getEnrollmentWeight(assignment, section, this) > section.getLimit())
396                        continue;
397                    if (config.getOffering().hasReservations()) {
398                        boolean hasReservation = false, hasSectionReservation = false, reservationMustBeUsed = false;
399                        for (Reservation r: getReservations(course)) {
400                            if (r.mustBeUsed()) reservationMustBeUsed = true;
401                            if (availableOnly && r.getReservedAvailableSpace(assignment, this) < getWeight()) continue;
402                            if (r.getSections(subpart) == null) {
403                                hasReservation = true;
404                            } else if (r.getSections(subpart).contains(section)) {
405                                hasReservation = true;
406                                hasSectionReservation = true;
407                            }
408                        }
409                        if (!hasSectionReservation && section.getTotalUnreservedSpace() < getWeight())
410                            continue;
411                        if (availableOnly && !hasSectionReservation && section.getUnreservedSpace(assignment, this) < getWeight())
412                            continue;
413                        if (!hasReservation && reservationMustBeUsed)
414                            continue;
415                    }
416                }
417                if (!getStudent().isAllowDisabled() && !section.isEnabled()) {
418                    boolean allowDisabled = false;
419                    for (Reservation r: getReservations(course)) {
420                        if (!r.isAllowDisabled()) continue;
421                        if (r.getSections(subpart) != null && !r.getSections(subpart).contains(section)) continue;
422                        if (!r.getConfigs().isEmpty() && !r.getConfigs().contains(config)) continue;
423                        allowDisabled = true; break;
424                    }
425                    if (!allowDisabled) continue;
426                }
427                if (skipSameTime && section.getTime() != null && !hasChildren && !times.add(section.getTime()) && !isSelected(section) && !isWaitlisted(section) && 
428                        (section.getIgnoreConflictWithSectionIds() == null || section.getIgnoreConflictWithSectionIds().isEmpty()))
429                    continue;
430                matchingSectionsThisSubpart.add(section);
431            }
432            if (random || limit > 0) {
433                sectionsThisSubpart = new ArrayList<Section>(sectionsThisSubpart);
434                Collections.shuffle(sectionsThisSubpart);
435            }
436            int i = 0;
437            for (Section section: matchingSectionsThisSubpart) {
438                sections.add(section);
439                computeEnrollments(assignment, enrollments, priority, penalty + section.getPenalty(), course, config, sections, idx + 1,
440                        availableOnly, skipSameTime, selectedOnly, random,
441                        limit < 0 ? limit : Math.max(1, limit * (1 + i) / matchingSectionsThisSubpart.size()));
442                sections.remove(section);
443                i++;
444            }
445        }
446    }
447
448    /** Return all enrollments that are available 
449     * @param assignment current assignment
450     * @return all available enrollments
451     **/
452    public List<Enrollment> getAvaiableEnrollments(Assignment<Request, Enrollment> assignment) {
453        List<Enrollment> ret = new ArrayList<Enrollment>();
454        int idx = 0;
455        for (Course course : iCourses) {
456            for (Config config : course.getOffering().getConfigs()) {
457                computeEnrollments(assignment, ret, idx, 0, course, config, new HashSet<Section>(), 0, true, false, false, false,
458                        getMaxDomainSize() <= 0 ? -1 : ret.size() + getMaxDomainSize());
459            }
460            idx++;
461        }
462        return ret;
463    }
464
465    /**
466     * Return all enrollments of the first course that are selected (
467     * {@link CourseRequest#isSelected(Section)} is true)
468     * 
469     * @param assignment current assignment
470     * @param availableOnly
471     *            pick only available sections
472     * @return selected enrollments
473     */
474    public List<Enrollment> getSelectedEnrollments(Assignment<Request, Enrollment> assignment, boolean availableOnly) {
475        if (getSelectedChoices().isEmpty())
476            return null;
477        List<Enrollment> enrollments = new ArrayList<Enrollment>();
478        for (Course course : iCourses) {
479            boolean hasChoice = false;
480            for (Choice choice: getSelectedChoices())
481                if (course.getOffering().equals(choice.getOffering())) { hasChoice = true; break; }
482            if (hasChoice)
483                for (Config config : course.getOffering().getConfigs()) {
484                    computeEnrollments(assignment, enrollments, 0, 0, course, config, new HashSet<Section>(), 0, availableOnly, false, true, false, -1);
485                }
486            break;
487        }
488        return enrollments;
489    }
490
491    /**
492     * Return all enrollments that are available, pick only the first section of
493     * the sections with the same time (of each subpart, {@link Section}
494     * comparator is used)
495     * @param assignment current assignment
496     * @return available enrollments 
497     */
498    public List<Enrollment> getAvaiableEnrollmentsSkipSameTime(Assignment<Request, Enrollment> assignment) {
499        List<Enrollment> ret = new ArrayList<Enrollment>();
500        if (getInitialAssignment() != null)
501            ret.add(getInitialAssignment());
502        int idx = 0;
503        for (Course course : iCourses) {
504            boolean skipSameTime = true;
505            for (LinkedSections link: getStudent().getLinkedSections())
506                if (link.getOfferings().contains(course.getOffering())) { skipSameTime = false; break; }
507            for (Config config : course.getOffering().getConfigs()) {
508                computeEnrollments(assignment, ret, idx, 0, course, config, new HashSet<Section>(), 0, true, skipSameTime, false, false,
509                        getMaxDomainSize() <= 0 ? -1 : ret.size() + getMaxDomainSize());
510            }
511            idx++;
512        }
513        return ret;
514    }
515
516    /**
517     * Return all possible enrollments, but pick only the first section of
518     * the sections with the same time (of each subpart, {@link Section}
519     * comparator is used).
520     * @param assignment current assignment
521     * @return computed enrollments 
522     */
523    public List<Enrollment> getEnrollmentsSkipSameTime(Assignment<Request, Enrollment> assignment) {
524        List<Enrollment> ret = new ArrayList<Enrollment>();
525        int idx = 0;
526        for (Course course : iCourses) {
527            for (Config config : course.getOffering().getConfigs()) {
528                boolean skipSameTime = true;
529                for (LinkedSections link: getStudent().getLinkedSections())
530                    if (link.getOfferings().contains(course.getOffering())) { skipSameTime = false; break; }
531                computeEnrollments(assignment, ret, idx, 0, course, config, new HashSet<Section>(), 0, false, skipSameTime, false, false,
532                        getMaxDomainSize() <= 0 ? -1 : ret.size() + getMaxDomainSize());
533            }
534            idx++;
535        }
536        return ret;
537    }
538
539    /** Wait-listed choices 
540     * @return wait-listed choices
541     **/
542    public Set<Choice> getWaitlistedChoices() {
543        return iWaitlistedChoices;
544    }
545
546    /**
547     * Return true when the given section is wait-listed (i.e., its choice is
548     * among wait-listed choices)
549     * @param section given section
550     * @return true if the given section matches the wait-listed choices
551     */
552    public boolean isWaitlisted(Section section) {
553        for (Choice choice: iWaitlistedChoices)
554            if (choice.sameChoice(section)) return true;
555        return false;
556    }
557
558    /** Selected choices 
559     * @return selected choices
560     **/
561    public Set<Choice> getSelectedChoices() {
562        return iSelectedChoices;
563    }
564
565    /**
566     * Return true when the given section is selected (i.e., its choice is among
567     * selected choices)
568     * @param section given section
569     * @return true if the given section matches the selected choices
570     */
571    public boolean isSelected(Section section) {
572        boolean hasMatch = false;
573        for (Choice choice: iSelectedChoices) {
574            if (choice.sameChoice(section) || choice.sameConfiguration(section)) return true;
575            if (choice.isMatching(section)) hasMatch = true;
576        }
577        return !iSelectedChoices.isEmpty() && !hasMatch;
578    }
579
580    /**
581     * Request name: A for alternative, 1 + priority, (w) when wait-list, list of
582     * course names
583     */
584    @Override
585    public String getName() {
586        String ret = (isAlternative() ? "A" : "")
587                + (1 + getPriority() + (isAlternative() ? -getStudent().nrRequests() : 0)) + ". "
588                + (isWaitlist() ? "(w) " : "");
589        int idx = 0;
590        for (Course course : iCourses) {
591            if (idx == 0)
592                ret += course.getName();
593            else
594                ret += ", " + idx + ". alt " + course.getName();
595            idx++;
596        }
597        return ret;
598    }
599
600    /**
601     * True if the student can be put on a wait-list (no alternative course
602     * request will be given instead)
603     * @return true if the request can be wait-listed
604     */
605    public boolean isWaitlist() {
606        return iWaitlist;
607    }
608    
609    /**
610     * True if the student can be put on a wait-list (no alternative course
611     * request will be given instead)
612     * @param waitlist true if the request can be wait-listed
613     */
614    public void setWaitlist(boolean waitlist) {
615        iWaitlist = waitlist;
616    }
617    
618    /**
619     * Time stamp of the request
620     * @return request time stamp
621     */
622    public Long getTimeStamp() {
623        return iTimeStamp;
624    }
625
626    @Override
627    public String toString() {
628        return getName() + (getWeight() != 1.0 ? " (W:" + sDF.format(getWeight()) + ")" : "");
629    }
630
631    /** Return course of the requested courses with the given id 
632     * @param courseId course offering id
633     * @return course of the given id
634     **/
635    public Course getCourse(long courseId) {
636        for (Course course : iCourses) {
637            if (course.getId() == courseId)
638                return course;
639        }
640        return null;
641    }
642
643    /** Return configuration of the requested courses with the given id 
644     * @param configId instructional offering configuration unique id
645     * @return config of the given id
646     **/
647    public Config getConfig(long configId) {
648        for (Course course : iCourses) {
649            for (Config config : course.getOffering().getConfigs()) {
650                if (config.getId() == configId)
651                    return config;
652            }
653        }
654        return null;
655    }
656
657    /** Return subpart of the requested courses with the given id 
658     * @param subpartId scheduling subpart unique id
659     * @return subpart of the given id
660     **/
661    public Subpart getSubpart(long subpartId) {
662        for (Course course : iCourses) {
663            for (Config config : course.getOffering().getConfigs()) {
664                for (Subpart subpart : config.getSubparts()) {
665                    if (subpart.getId() == subpartId)
666                        return subpart;
667                }
668            }
669        }
670        return null;
671    }
672
673    /** Return section of the requested courses with the given id 
674     * @param sectionId class unique id
675     * @return section of the given id
676     **/
677    public Section getSection(long sectionId) {
678        for (Course course : iCourses) {
679            for (Config config : course.getOffering().getConfigs()) {
680                for (Subpart subpart : config.getSubparts()) {
681                    for (Section section : subpart.getSections()) {
682                        if (section.getId() == sectionId)
683                            return section;
684                    }
685                }
686            }
687        }
688        return null;
689    }
690
691    /**
692     * Minimal penalty (minimum of {@link Offering#getMinPenalty()} among
693     * requested courses)
694     * @return minimal penalty
695     */
696    public double getMinPenalty() {
697        if (iCachedMinPenalty == null) {
698            double min = Double.MAX_VALUE;
699            for (Course course : iCourses) {
700                min = Math.min(min, course.getOffering().getMinPenalty());
701            }
702            iCachedMinPenalty = new Double(min);
703        }
704        return iCachedMinPenalty.doubleValue();
705    }
706
707    /**
708     * Maximal penalty (maximum of {@link Offering#getMaxPenalty()} among
709     * requested courses)
710     * @return maximal penalty
711     */
712    public double getMaxPenalty() {
713        if (iCachedMaxPenalty == null) {
714            double max = Double.MIN_VALUE;
715            for (Course course : iCourses) {
716                max = Math.max(max, course.getOffering().getMaxPenalty());
717            }
718            iCachedMaxPenalty = new Double(max);
719        }
720        return iCachedMaxPenalty.doubleValue();
721    }
722
723    /** Clear cached min/max penalties and cached bound */
724    public void clearCache() {
725        iCachedMaxPenalty = null;
726        iCachedMinPenalty = null;
727    }
728
729    /**
730     * Estimated bound for this request -- it estimates the smallest value among
731     * all possible enrollments
732     */
733    @Override
734    public double getBound() {
735        return - getWeight() * ((StudentSectioningModel)getModel()).getStudentWeights().getBound(this);
736        /*
737        if (iCachedBound == null) {
738            iCachedBound = new Double(-Math.pow(Enrollment.sPriorityWeight, getPriority())
739                    * (isAlternative() ? Enrollment.sAlterativeWeight : 1.0)
740                    * Math.pow(Enrollment.sInitialWeight, (getInitialAssignment() == null ? 0 : 1))
741                    * Math.pow(Enrollment.sSelectedWeight, (iSelectedChoices.isEmpty() ? 0 : 1))
742                    * Math.pow(Enrollment.sWaitlistedWeight, (iWaitlistedChoices.isEmpty() ? 0 : 1))
743                    *
744                    // Math.max(Enrollment.sMinWeight,getWeight()) *
745                    (getStudent().isDummy() ? Student.sDummyStudentWeight : 1.0)
746                    * Enrollment.normalizePenalty(getMinPenalty()));
747        }
748        return iCachedBound.doubleValue();
749        */
750    }
751
752    /** Return true if request is assigned. */
753    @Override
754    public boolean isAssigned(Assignment<Request, Enrollment> assignment) {
755        Enrollment e = assignment.getValue(this);
756        return e != null && !e.getAssignments().isEmpty();
757    }
758
759    @Override
760    public boolean equals(Object o) {
761        return super.equals(o) && (o instanceof CourseRequest);
762    }
763    
764    /**
765     * Get reservations for this course requests
766     * @param course given course
767     * @return reservations for this course requests and the given course
768     */
769    public synchronized List<Reservation> getReservations(Course course) {
770        if (iReservations == null)
771            iReservations = new HashMap<Course, List<Reservation>>();
772        List<Reservation> reservations = iReservations.get(course);
773        if (reservations == null) {
774            reservations = new ArrayList<Reservation>();
775            boolean mustBeUsed = false;
776            for (Reservation r: course.getOffering().getReservations()) {
777                if (!r.isApplicable(getStudent())) continue;
778                if (!mustBeUsed && r.mustBeUsed()) { reservations.clear(); mustBeUsed = true; }
779                if (mustBeUsed && !r.mustBeUsed()) continue;
780                reservations.add(r);
781            }
782            iReservations.put(course, reservations);
783        }
784        return reservations;
785    }
786    private Map<Course, List<Reservation>> iReservations = null;
787    
788    /**
789     * Get reservations for this course requests ordered using {@link Reservation#compareTo(Assignment, Reservation)}
790     * @param course given course
791     * @return reservations for this course requests and the given course
792     */
793    public TreeSet<Reservation> getSortedReservations(Assignment<Request, Enrollment> assignment, Course course) {
794        TreeSet<Reservation> reservations = new TreeSet<Reservation>(new AssignmentComparator<Reservation, Request, Enrollment>(assignment));
795        reservations.addAll(getReservations(course));
796        return reservations;
797    }
798    
799    /**
800     * Return true if there is a reservation for a course of this request
801     * @return true if there is a reservation for a course of this request
802     */
803    public boolean hasReservations() {
804        for (Course course: getCourses())
805            if (!getReservations(course).isEmpty())
806                return true;
807        return false;
808    }
809    
810    /**
811     * Clear reservation information that was cached on this section
812     */
813    public synchronized void clearReservationCache() {
814        if (iReservations != null) iReservations.clear();
815    }
816    
817    /**
818     * Return true if this request can track MPP
819     * @return true if the request is course request and it either has an initial enrollment or some selected choices.
820     */
821    @Override
822    public boolean isMPP() {
823        StudentSectioningModel model = (StudentSectioningModel) getModel();
824        if (model == null || !model.isMPP()) return false;
825        return !getStudent().isDummy() && (getInitialAssignment() != null || !getSelectedChoices().isEmpty()); 
826    }
827    
828    /**
829     * Return true if this request has any selection
830     * @return true if the request is course request and has some selected choices.
831     */
832    @Override
833    public boolean hasSelection() {
834        if (getStudent().isDummy() || getSelectedChoices().isEmpty()) return false;
835        for (Choice choice: getSelectedChoices())
836            if (choice.getSectionId() != null || choice.getConfigId() != null) return true;
837        return false;
838    }
839    
840    /**
841     * Add request group to this request.
842     * @param group request group to be added
843     */
844    public void addRequestGroup(RequestGroup group) {
845        iRequestGroups.add(group);
846        group.addRequest(this);
847    }
848    
849    /**
850     * Removed request group from this request.
851     * @param group request group to be removed
852     */
853    public void removeRequestGroup(RequestGroup group) {
854        iRequestGroups.remove(group);
855        group.removeRequest(this);
856    }
857
858    /**
859     * Lists request groups of this request
860     * @return request groups of this course requests
861     */
862    public Set<RequestGroup> getRequestGroups() {
863        return iRequestGroups;
864    }
865    
866    @Override
867    public void variableAssigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment enrollment) {
868        super.variableAssigned(assignment, iteration, enrollment);
869        for (RequestGroup g: getRequestGroups())
870            if (g.getCourse().equals(enrollment.getCourse()))
871                g.assigned(assignment, enrollment);
872    }
873
874    @Override
875    public void variableUnassigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment enrollment) {
876        super.variableUnassigned(assignment, iteration, enrollment);
877        for (RequestGroup g: getRequestGroups())
878            if (g.getCourse().equals(enrollment.getCourse()))
879                g.unassigned(assignment, enrollment);
880    }
881}