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