    001package net.sf.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 net.sf.cpsolver.coursett.model.TimeLocation;
    015import net.sf.cpsolver.ifs.util.ToolBox;
    016import net.sf.cpsolver.studentsct.StudentSectioningModel;
    017import net.sf.cpsolver.studentsct.constraint.ConfigLimit;
    018import net.sf.cpsolver.studentsct.constraint.CourseLimit;
    019import net.sf.cpsolver.studentsct.constraint.SectionLimit;
    020import 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 Tomáš Müller<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 */
    046public 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     * Maximal domain size (i.e., number of enrollments of a course request), -1 if there is no limit.
    127     */
    128    protected int getMaxDomainSize() {
    129        StudentSectioningModel model = (StudentSectioningModel) getModel();
    130        return model == null ? null : model.getMaxDomainSize();
    131    }
    132
    133    /**
    134     * Return all possible enrollments.
    135     */
    136    @Override
    137    public List<Enrollment> computeEnrollments() {
    138        List<Enrollment> ret = new ArrayList<Enrollment>();
    139        int idx = 0;
    140        for (Course course : iCourses) {
    141            for (Config config : course.getOffering().getConfigs()) {
    142                computeEnrollments(ret, idx, 0, course, config, new HashSet<Section>(), 0, false, false,
    143                        false, false, getMaxDomainSize() <= 0 ? -1 : ret.size() + getMaxDomainSize());
    144            }
    145            idx++;
    146        }
    147        return ret;
    148    }
    149
    150    /**
    151     * Return a subset of all enrollments -- randomly select only up to
    152     * limitEachConfig enrollments of each config.
    153     */
    154    public List<Enrollment> computeRandomEnrollments(int limitEachConfig) {
    155        List<Enrollment> ret = new ArrayList<Enrollment>();
    156        int idx = 0;
    157        for (Course course : iCourses) {
    158            for (Config config : course.getOffering().getConfigs()) {
    159                computeEnrollments(ret, idx, 0, course, config, new HashSet<Section>(), 0, false, false,
    160                        false, true, (limitEachConfig <= 0 ? limitEachConfig : ret.size() + limitEachConfig));
    161            }
    162            idx++;
    163        }
    164        return ret;
    165    }
    166
    167    /**
    168     * Return true if the both sets of sections contain sections of the same
    169     * subparts, and each pair of sections of the same subpart is offered at the
    170     * same time.
    171     */
    172    private boolean sameTimes(Set<Section> sections1, Set<Section> sections2) {
    173        for (Section s1 : sections1) {
    174            Section s2 = null;
    175            for (Section s : sections2) {
    176                if (s.getSubpart().equals(s1.getSubpart())) {
    177                    s2 = s;
    178                    break;
    179                }
    180            }
    181            if (s2 == null)
    182                return false;
    183            if (!ToolBox.equals(s1.getTime(), s2.getTime()))
    184                return false;
    185        }
    186        return true;
    187    }
    188
    189    /**
    190     * Recursive computation of enrollments
    191     *
    192     * @param enrollments
    193     *            list of enrollments to be returned
    194     * @param priority
    195     *            zero for the course, one for the first alternative, two for the second alternative
    196     * @param penalty
    197     *            penalty of the selected sections
    198     * @param course
    199     *            selected course
    200     * @param config
    201     *            selected configuration
    202     * @param sections
    203     *            sections selected so far
    204     * @param idx
    205     *            index of the subparts (a section of 0..idx-1 subparts has been
    206     *            already selected)
    207     * @param availableOnly
    208     *            only use available sections
    209     * @param skipSameTime
    210     *            for each possible times, pick only one section
    211     * @param selectedOnly
    212     *            select only sections that are selected (
    213     *            {@link CourseRequest#isSelected(Section)} is true)
    214     * @param random
    215     *            pick sections in a random order (useful when limit is used)
    216     * @param limit
    217     *            when above zero, limit the number of selected enrollments to
    218     *            this limit
    219     * @param reservations
    220     *            list of applicable reservations
    221     */
    222    private void computeEnrollments(Collection<Enrollment> enrollments, int priority, double penalty, Course course, Config config,
    223            HashSet<Section> sections, int idx, boolean availableOnly, boolean skipSameTime, boolean selectedOnly,
    224            boolean random, int limit) {
    225        if (limit > 0 && enrollments.size() >= limit)
    226            return;
    227        if (idx == 0) { // run only once for each configuration
    228            boolean canOverLimit = false;
    229            if (availableOnly && (getModel() == null || ((StudentSectioningModel)getModel()).getReservationCanAssignOverTheLimit())) {
    230                for (Reservation r: getReservations(course)) {
    231                    if (!r.canAssignOverLimit()) continue;
    232                    if (!r.getConfigs().isEmpty() && !r.getConfigs().contains(config)) continue;
    233                    if (r.getReservedAvailableSpace(this) < getWeight()) continue;
    234                    canOverLimit = true; break;
    235                }
    236            }
    237            if (!canOverLimit) {
    238                if (availableOnly && config.getLimit() >= 0 && ConfigLimit.getEnrollmentWeight(config, this) > config.getLimit())
    239                    return;
    240                if (availableOnly && course.getLimit() >= 0 && CourseLimit.getEnrollmentWeight(course, this) > course.getLimit())
    241                    return;
    242                if (config.getOffering().hasReservations()) {
    243                    boolean hasReservation = false, hasOtherReservation = false, hasConfigReservation = false, reservationMustBeUsed = false;
    244                    for (Reservation r: getReservations(course)) {
    245                        if (r.mustBeUsed()) reservationMustBeUsed = true;
    246                        if (availableOnly && r.getReservedAvailableSpace(this) < getWeight()) continue;
    247                        if (r.getConfigs().isEmpty()) {
    248                            hasReservation = true;
    249                        } else if (r.getConfigs().contains(config)) {
    250                            hasReservation = true;
    251                            hasConfigReservation = true;
    252                        } else {
    253                            hasOtherReservation = true;
    254                        }
    255                    }
    256                    if (!hasConfigReservation && config.getTotalUnreservedSpace() < getWeight())
    257                        return;
    258                    if (!hasReservation && config.getOffering().getTotalUnreservedSpace() < getWeight())
    259                        return;
    260                    if (hasOtherReservation && !hasReservation)
    261                        return;
    262                    if (availableOnly && !hasReservation && config.getOffering().getUnreservedSpace(this) < getWeight())
    263                        return;
    264                    if (availableOnly && !hasConfigReservation && config.getUnreservedSpace(this) < getWeight())
    265                        return;
    266                    if (!hasReservation && reservationMustBeUsed)
    267                        return;
    268                }
    269            }
    270        }
    271        if (config.getSubparts().size() == idx) {
    272            if (skipSameTime && sSameTimePrecise) {
    273                boolean waitListedOrSelected = false;
    274                if (!getSelectedChoices().isEmpty() || !getWaitlistedChoices().isEmpty()) {
    275                    for (Section section : sections) {
    276                        if (isWaitlisted(section) || isSelected(section)) {
    277                            waitListedOrSelected = true;
    278                            break;
    279                        }
    280                    }
    281                }
    282                if (!waitListedOrSelected) {
    283                    for (Enrollment enrollment : enrollments) {
    284                        if (sameTimes(enrollment.getSections(), sections))
    285                            return;
    286                    }
    287                }
    288            }
    289            if (!config.getOffering().hasReservations()) {
    290                enrollments.add(new Enrollment(this, priority, null, config, new HashSet<Assignment>(sections), null));
    291            } else {
    292                Enrollment e = new Enrollment(this, priority, null, config, new HashSet<Assignment>(sections), null);
    293                boolean mustHaveReservation = config.getOffering().getTotalUnreservedSpace() < getWeight();
    294                boolean mustHaveConfigReservation = config.getTotalUnreservedSpace() < getWeight();
    295                boolean mustHaveSectionReservation = false;
    296                for (Section s: sections) {
    297                    if (s.getTotalUnreservedSpace() < getWeight()) {
    298                        mustHaveSectionReservation = true;
    299                        break;
    300                    }
    301                }
    302                boolean canOverLimit = false;
    303                if (availableOnly &&  (getModel() == null || ((StudentSectioningModel)getModel()).getReservationCanAssignOverTheLimit())) {
    304                    for (Reservation r: getReservations(course)) {
    305                        if (!r.canAssignOverLimit() || !r.isIncluded(e)) continue;
    306                        if (r.getReservedAvailableSpace(this) < getWeight()) continue;
    307                        enrollments.add(new Enrollment(this, priority, null, config, new HashSet<Assignment>(sections), r));
    308                        canOverLimit = true;
    309                    }
    310                }
    311                if (!canOverLimit) {
    312                    boolean reservationMustBeUsed = false;
    313                    reservations: for (Reservation r: (availableOnly ? new TreeSet<Reservation>(getReservations(course)) : getReservations(course))) {
    314                        if (r.mustBeUsed()) reservationMustBeUsed = true;
    315                        if (!r.isIncluded(e)) continue;
    316                        if (availableOnly && r.getReservedAvailableSpace(this) < getWeight()) continue;
    317                        if (mustHaveConfigReservation && r.getConfigs().isEmpty()) continue;
    318                        if (mustHaveSectionReservation)
    319                            for (Section s: sections)
    320                                if (r.getSections(s.getSubpart()) == null && s.getTotalUnreservedSpace() < getWeight()) continue reservations;
    321                        enrollments.add(new Enrollment(this, priority, null, config, new HashSet<Assignment>(sections), r));
    322                        if (availableOnly) return; // only one available reservation suffice (the best matching one)
    323                    }
    324                    // a case w/o reservation
    325                    if (!(mustHaveReservation || mustHaveConfigReservation || mustHaveSectionReservation) &&
    326                        !(availableOnly && config.getOffering().getUnreservedSpace(this) < getWeight()) &&
    327                        !reservationMustBeUsed) {
    328                        enrollments.add(new Enrollment(this, (getReservations(course).isEmpty() ? 0 : 1) + priority, null, config, new HashSet<Assignment>(sections), null));
    329                    }
    330                }
    331            }
    332        } else {
    333            Subpart subpart = config.getSubparts().get(idx);
    334            HashSet<TimeLocation> times = (skipSameTime ? new HashSet<TimeLocation>() : null);
    335            List<Section> sectionsThisSubpart = subpart.getSections();
    336            if (skipSameTime) {
    337                sectionsThisSubpart = new ArrayList<Section>(subpart.getSections());
    338                Collections.sort(sectionsThisSubpart);
    339            }
    340            List<Section> matchingSectionsThisSubpart = new ArrayList<Section>(subpart.getSections().size());
    341            boolean hasChildren = !subpart.getChildren().isEmpty();
    342            for (Section section : sectionsThisSubpart) {
    343                if (section.getParent() != null && !sections.contains(section.getParent()))
    344                    continue;
    345                if (section.isOverlapping(sections))
    346                    continue;
    347                if (selectedOnly && !isSelected(section))
    348                    continue;
    349                boolean canOverLimit = false;
    350                if (availableOnly && (getModel() == null || ((StudentSectioningModel)getModel()).getReservationCanAssignOverTheLimit())) {
    351                    for (Reservation r: getReservations(course)) {
    352                        if (!r.canAssignOverLimit()) continue;
    353                        if (r.getSections(subpart) != null && !r.getSections(subpart).contains(section)) continue;
    354                        if (r.getReservedAvailableSpace(this) < getWeight()) continue;
    355                        canOverLimit = true; break;
    356                    }
    357                }
    358                if (!canOverLimit) {
    359                    if (availableOnly && section.getLimit() >= 0
    360                            && SectionLimit.getEnrollmentWeight(section, this) > section.getLimit())
    361                        continue;
    362                    if (config.getOffering().hasReservations()) {
    363                        boolean hasReservation = false, hasSectionReservation = false, hasOtherReservation = false, reservationMustBeUsed = false;
    364                        for (Reservation r: getReservations(course)) {
    365                            if (r.mustBeUsed()) reservationMustBeUsed = true;
    366                            if (availableOnly && r.getReservedAvailableSpace(this) < getWeight()) continue;
    367                            if (r.getSections(subpart) == null) {
    368                                hasReservation = true;
    369                            } else if (r.getSections(subpart).contains(section)) {
    370                                hasReservation = true;
    371                                hasSectionReservation = true;
    372                            } else {
    373                                hasOtherReservation = true;
    374                            }
    375                        }
    376                        if (!hasSectionReservation && section.getTotalUnreservedSpace() < getWeight())
    377                            continue;
    378                        if (hasOtherReservation && !hasReservation)
    379                            continue;
    380                        if (availableOnly && !hasSectionReservation && section.getUnreservedSpace(this) < getWeight())
    381                            continue;
    382                        if (!hasReservation && reservationMustBeUsed)
    383                            continue;
    384                    }
    385                }
    386                if (skipSameTime && section.getTime() != null && !hasChildren && !times.add(section.getTime()) && !isSelected(section) && !isWaitlisted(section))
    387                    continue;
    388                matchingSectionsThisSubpart.add(section);
    389            }
    390            if (random || limit > 0)
    391                Collections.shuffle(sectionsThisSubpart);
    392            int i = 0;
    393            for (Section section: matchingSectionsThisSubpart) {
    394                sections.add(section);
    395                computeEnrollments(enrollments, priority, penalty + section.getPenalty(), course, config, sections, idx + 1,
    396                        availableOnly, skipSameTime, selectedOnly, random,
    397                        limit < 0 ? limit : Math.max(1, limit * (1 + i) / matchingSectionsThisSubpart.size())
    398                        );
    399                sections.remove(section);
    400                i++;
    401            }
    402        }
    403    }
    404
    405    /** Return all enrollments that are available */
    406    public List<Enrollment> getAvaiableEnrollments() {
    407        List<Enrollment> ret = new ArrayList<Enrollment>();
    408        int idx = 0;
    409        for (Course course : iCourses) {
    410            for (Config config : course.getOffering().getConfigs()) {
    411                computeEnrollments(ret, idx, 0, course, config, new HashSet<Section>(), 0, true, false, false, false,
    412                        getMaxDomainSize() <= 0 ? -1 : ret.size() + getMaxDomainSize());
    413            }
    414            idx++;
    415        }
    416        return ret;
    417    }
    418
    419    /**
    420     * Return all enrollments that are selected (
    421     * {@link CourseRequest#isSelected(Section)} is true)
    422     *
    423     * @param availableOnly
    424     *            pick only available sections
    425     */
    426    public List<Enrollment> getSelectedEnrollments(boolean availableOnly) {
    427        if (getSelectedChoices().isEmpty())
    428            return null;
    429        Choice firstChoice = getSelectedChoices().iterator().next();
    430        List<Enrollment> enrollments = new ArrayList<Enrollment>();
    431        for (Course course : iCourses) {
    432            if (!course.getOffering().equals(firstChoice.getOffering()))
    433                continue;
    434            for (Config config : course.getOffering().getConfigs()) {
    435                computeEnrollments(enrollments, 0, 0, course, config, new HashSet<Section>(), 0, availableOnly, false, true, false, -1);
    436            }
    437        }
    438        return enrollments;
    439    }
    440
    441    /**
    442     * Return all enrollments that are available, pick only the first section of
    443     * the sections with the same time (of each subpart, {@link Section}
    444     * comparator is used)
    445     */
    446    public List<Enrollment> getAvaiableEnrollmentsSkipSameTime() {
    447        List<Enrollment> ret = new ArrayList<Enrollment>();
    448        if (getInitialAssignment() != null)
    449            ret.add(getInitialAssignment());
    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, true, true, false, false,
    454                        getMaxDomainSize() <= 0 ? -1 : ret.size() + getMaxDomainSize());
    455            }
    456            idx++;
    457        }
    458        return ret;
    459    }
    460
    461    /**
    462     * Return all possible enrollments.
    463     */
    464    public List<Enrollment> getEnrollmentsSkipSameTime() {
    465        List<Enrollment> ret = new ArrayList<Enrollment>();
    466        int idx = 0;
    467        for (Course course : iCourses) {
    468            for (Config config : course.getOffering().getConfigs()) {
    469                computeEnrollments(ret, idx, 0, course, config, new HashSet<Section>(), 0, false, true, false, false,
    470                        getMaxDomainSize() <= 0 ? -1 : ret.size() + getMaxDomainSize());
    471            }
    472            idx++;
    473        }
    474        return ret;
    475    }
    476
    477    /** Wait-listed choices */
    478    public Set<Choice> getWaitlistedChoices() {
    479        return iWaitlistedChoices;
    480    }
    481
    482    /**
    483     * Return true when the given section is wait-listed (i.e., its choice is
    484     * among wait-listed choices)
    485     */
    486    public boolean isWaitlisted(Section section) {
    487        return iWaitlistedChoices.contains(section.getChoice());
    488    }
    489
    490    /** Selected choices */
    491    public Set<Choice> getSelectedChoices() {
    492        return iSelectedChoices;
    493    }
    494
    495    /**
    496     * Return true when the given section is selected (i.e., its choice is among
    497     * selected choices)
    498     */
    499    public boolean isSelected(Section section) {
    500        return iSelectedChoices.contains(section.getChoice());
    501    }
    502
    503    /**
    504     * Request name: A for alternative, 1 + priority, (w) when waitlist, list of
    505     * course names
    506     */
    507    @Override
    508    public String getName() {
    509        String ret = (isAlternative() ? "A" : "")
    510                + (1 + getPriority() + (isAlternative() ? -getStudent().nrRequests() : 0)) + ". "
    511                + (isWaitlist() ? "(w) " : "");
    512        int idx = 0;
    513        for (Course course : iCourses) {
    514            if (idx == 0)
    515                ret += course.getName();
    516            else
    517                ret += ", " + idx + ". alt " + course.getName();
    518            idx++;
    519        }
    520        return ret;
    521    }
    522
    523    /**
    524     * True if the student can be put on a wait-list (no alternative course
    525     * request will be given instead)
    526     */
    527    public boolean isWaitlist() {
    528        return iWaitlist;
    529    }
    530
    531    /**
    532     * True if the student can be put on a wait-list (no alternative course
    533     * request will be given instead)
    534     */
    535    public void setWaitlist(boolean waitlist) {
    536        iWaitlist = waitlist;
    537    }
    538
    539    /**
    540     * Time stamp of the request
    541     */
    542    public Long getTimeStamp() {
    543        return iTimeStamp;
    544    }
    545
    546    @Override
    547    public String toString() {
    548        return getName() + (getWeight() != 1.0 ? " (W:" + sDF.format(getWeight()) + ")" : "");
    549    }
    550
    551    /** Return course of the requested courses with the given id */
    552    public Course getCourse(long courseId) {
    553        for (Course course : iCourses) {
    554            if (course.getId() == courseId)
    555                return course;
    556        }
    557        return null;
    558    }
    559
    560    /** Return configuration of the requested courses with the given id */
    561    public Config getConfig(long configId) {
    562        for (Course course : iCourses) {
    563            for (Config config : course.getOffering().getConfigs()) {
    564                if (config.getId() == configId)
    565                    return config;
    566            }
    567        }
    568        return null;
    569    }
    570
    571    /** Return subpart of the requested courses with the given id */
    572    public Subpart getSubpart(long subpartId) {
    573        for (Course course : iCourses) {
    574            for (Config config : course.getOffering().getConfigs()) {
    575                for (Subpart subpart : config.getSubparts()) {
    576                    if (subpart.getId() == subpartId)
    577                        return subpart;
    578                }
    579            }
    580        }
    581        return null;
    582    }
    583
    584    /** Return section of the requested courses with the given id */
    585    public Section getSection(long sectionId) {
    586        for (Course course : iCourses) {
    587            for (Config config : course.getOffering().getConfigs()) {
    588                for (Subpart subpart : config.getSubparts()) {
    589                    for (Section section : subpart.getSections()) {
    590                        if (section.getId() == sectionId)
    591                            return section;
    592                    }
    593                }
    594            }
    595        }
    596        return null;
    597    }
    598
    599    /**
    600     * Minimal penalty (minimum of {@link Offering#getMinPenalty()} among
    601     * requested courses)
    602     */
    603    public double getMinPenalty() {
    604        if (iCachedMinPenalty == null) {
    605            double min = Double.MAX_VALUE;
    606            for (Course course : iCourses) {
    607                min = Math.min(min, course.getOffering().getMinPenalty());
    608            }
    609            iCachedMinPenalty = new Double(min);
    610        }
    611        return iCachedMinPenalty.doubleValue();
    612    }
    613
    614    /**
    615     * Maximal penalty (maximum of {@link Offering#getMaxPenalty()} among
    616     * requested courses)
    617     */
    618    public double getMaxPenalty() {
    619        if (iCachedMaxPenalty == null) {
    620            double max = Double.MIN_VALUE;
    621            for (Course course : iCourses) {
    622                max = Math.max(max, course.getOffering().getMaxPenalty());
    623            }
    624            iCachedMaxPenalty = new Double(max);
    625        }
    626        return iCachedMaxPenalty.doubleValue();
    627    }
    628
    629    /** Clear cached min/max penalties and cached bound */
    630    public void clearCache() {
    631        iCachedMaxPenalty = null;
    632        iCachedMinPenalty = null;
    633    }
    634
    635    /**
    636     * Estimated bound for this request -- it estimates the smallest value among
    637     * all possible enrollments
    638     */
    639    @Override
    640    public double getBound() {
    641        return - getWeight() * ((StudentSectioningModel)getModel()).getStudentWeights().getBound(this);
    642        /*
    643        if (iCachedBound == null) {
    644            iCachedBound = new Double(-Math.pow(Enrollment.sPriorityWeight, getPriority())
    645                    * (isAlternative() ? Enrollment.sAlterativeWeight : 1.0)
    646                    * Math.pow(Enrollment.sInitialWeight, (getInitialAssignment() == null ? 0 : 1))
    647                    * Math.pow(Enrollment.sSelectedWeight, (iSelectedChoices.isEmpty() ? 0 : 1))
    648                    * Math.pow(Enrollment.sWaitlistedWeight, (iWaitlistedChoices.isEmpty() ? 0 : 1))
    649                    *
    650                    // Math.max(Enrollment.sMinWeight,getWeight()) *
    651                    (getStudent().isDummy() ? Student.sDummyStudentWeight : 1.0)
    652                    * Enrollment.normalizePenalty(getMinPenalty()));
    653        }
    654        return iCachedBound.doubleValue();
    655        */
    656    }
    657
    658    /** Return true if request is assigned. */
    659    @Override
    660    public boolean isAssigned() {
    661        return getAssignment() != null && !(getAssignment()).getAssignments().isEmpty();
    662    }
    663
    664    @Override
    665    public boolean equals(Object o) {
    666        return super.equals(o) && (o instanceof CourseRequest);
    667    }
    668
    669    /**
    670     * Get reservations for this course requests
    671     */
    672    public List<Reservation> getReservations(Course course) {
    673        if (iReservations == null)
    674            iReservations = new HashMap<Course, List<Reservation>>();
    675        List<Reservation> reservations = iReservations.get(course);
    676        if (reservations == null) {
    677            reservations = new ArrayList<Reservation>();
    678            boolean mustBeUsed = false;
    679            for (Reservation r: course.getOffering().getReservations()) {
    680                if (!r.isApplicable(getStudent())) continue;
    681                if (!mustBeUsed && r.mustBeUsed()) { reservations.clear(); mustBeUsed = true; }
    682                if (mustBeUsed && !r.mustBeUsed()) continue;
    683                reservations.add(r);
    684            }
    685            iReservations.put(course, reservations);
    686        }
    687        return reservations;
    688    }
    689    private Map<Course, List<Reservation>> iReservations = null;
    690
    691    /**
    692     * Return true if there is a reservation for a course of this request
    693     */
    694    public boolean hasReservations() {
    695        for (Course course: getCourses())
    696            if (!getReservations(course).isEmpty())
    697                return true;
    698        return false;
    699    }
    700
    701    /**
    702     * Clear reservation information that was cached on this section
    703     */
    704    public void clearReservationCache() {
    705        if (iReservations != null) iReservations.clear();
    706    }
    707
    708}