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 }