    001package org.cpsolver.studentsct.model;
    002
    003import java.util.Collections;
    004import java.util.HashSet;
    005import java.util.Set;
    006import java.util.regex.Matcher;
    007import java.util.regex.Pattern;
    008
    009import org.cpsolver.ifs.assignment.Assignment;
    010import org.cpsolver.ifs.assignment.context.AbstractClassWithContext;
    011import org.cpsolver.ifs.assignment.context.AssignmentConstraintContext;
    012import org.cpsolver.ifs.assignment.context.CanInheritContext;
    013import org.cpsolver.ifs.model.Model;
    014
    015
    016/**
    017 * Representation of a course offering. A course offering contains id, subject
    018 * area, course number and an instructional offering. <br>
    019 * <br>
    020 * Each instructional offering (see {@link Offering}) is offered under one or
    021 * more course offerings.
    022 *
    023 * <br>
    024 * <br>
    025 *
    026 * @author  Tomáš Müller
    027 * @version StudentSct 1.3 (Student Sectioning)<br>
    028 *          Copyright (C) 2007 - 2014 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 Course extends AbstractClassWithContext<Request, Enrollment, Course.CourseContext> implements CanInheritContext<Request, Enrollment, Course.CourseContext> {
    047    private long iId = -1;
    048    private String iSubjectArea = null;
    049    private String iCourseNumber = null;
    050    private Offering iOffering = null;
    051    private int iLimit = 0, iProjected = 0;
    052    private Set<CourseRequest> iRequests = Collections.synchronizedSet(new HashSet<CourseRequest>());
    053    private String iNote = null;
    054    private Set<RequestGroup> iRequestGroups = new HashSet<RequestGroup>();
    055    private String iCredit = null;
    056    private Float iCreditValue = null;
    057    private String iTitle = null;
    058    private String iType = null;
    059    private Course iParent = null;
    060    private Set<Course> iChildren = null;
    061
    062    /**
    063     * Constructor
    064     *
    065     * @param id
    066     *            course offering unique id
    067     * @param subjectArea
    068     *            subject area (e.g., MA, CS, ENGL)
    069     * @param courseNumber
    070     *            course number under the given subject area
    071     * @param offering
    072     *            instructional offering which is offered under this course
    073     *            offering
    074     */
    075    public Course(long id, String subjectArea, String courseNumber, Offering offering) {
    076        iId = id;
    077        iSubjectArea = subjectArea;
    078        iCourseNumber = courseNumber;
    079        iOffering = offering;
    080        iOffering.getCourses().add(this);
    081    }
    082
    083    /**
    084     * Constructor
    085     *
    086     * @param id
    087     *            course offering unique id
    088     * @param subjectArea
    089     *            subject area (e.g., MA, CS, ENGL)
    090     * @param courseNumber
    091     *            course number under the given subject area
    092     * @param offering
    093     *            instructional offering which is offered under this course
    094     *            offering
    095     * @param limit
    096     *            course offering limit (-1 for unlimited)
    097     * @param projected
    098     *            projected demand
    099     */
    100    public Course(long id, String subjectArea, String courseNumber, Offering offering, int limit, int projected) {
    101        iId = id;
    102        iSubjectArea = subjectArea;
    103        iCourseNumber = courseNumber;
    104        iOffering = offering;
    105        iOffering.getCourses().add(this);
    106        iLimit = limit;
    107        iProjected = projected;
    108    }
    109
    110    /** Course offering unique id
    111     * @return coure offering unqiue id
    112     **/
    113    public long getId() {
    114        return iId;
    115    }
    116
    117    /** Subject area
    118     * @return subject area abbreviation
    119     **/
    120    public String getSubjectArea() {
    121        return iSubjectArea;
    122    }
    123
    124    /** Course number
    125     * @return course number
    126     **/
    127    public String getCourseNumber() {
    128        return iCourseNumber;
    129    }
    130
    131    /** Course offering name: subject area + course number
    132     * @return course name
    133     **/
    134    public String getName() {
    135        return iSubjectArea + " " + iCourseNumber;
    136    }
    137
    138    @Override
    139    public String toString() {
    140        return getName();
    141    }
    142
    143    /** Instructional offering which is offered under this course offering.
    144     * @return instructional offering
    145     **/
    146    public Offering getOffering() {
    147        return iOffering;
    148    }
    149
    150    /** Course offering limit
    151     * @return course offering limit, -1 if unlimited
    152     **/
    153    public int getLimit() {
    154        return iLimit;
    155    }
    156
    157    /** Set course offering limit
    158     * @param limit course offering limit, -1 if unlimited
    159     **/
    160    public void setLimit(int limit) {
    161        iLimit = limit;
    162    }
    163
    164    /** Course offering projected number of students
    165     * @return course projection
    166     **/
    167    public int getProjected() {
    168        return iProjected;
    169    }
    170
    171    /** Called when an enrollment with this course is assigned to a request
    172     * @param assignment current assignment
    173     * @param enrollment assigned enrollment
    174     **/
    175    public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    176        getContext(assignment).assigned(assignment, enrollment);
    177    }
    178
    179    /** Called when an enrollment with this course is unassigned from a request
    180     * @param assignment current assignment
    181     * @param enrollment unassigned enrollment
    182     */
    183    public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    184        getContext(assignment).unassigned(assignment, enrollment);
    185    }
    186
    187    /** Set of course requests requesting this course
    188     * @return request for this course
    189     **/
    190    public Set<CourseRequest> getRequests() {
    191        return iRequests;
    192    }
    193
    194    /**
    195     * Course note
    196     * @return course note
    197     */
    198    public String getNote() { return iNote; }
    199
    200    /**
    201     * Course note
    202     * @param note course note
    203     */
    204    public void setNote(String note) { iNote = note; }
    205
    206    @Override
    207    public boolean equals(Object o) {
    208        if (o == null || !(o instanceof Course)) return false;
    209        return getId() == ((Course)o).getId();
    210    }
    211
    212    @Override
    213    public int hashCode() {
    214        return (int) (iId ^ (iId >>> 32));
    215    }
    216
    217    @Override
    218    public Model<Request, Enrollment> getModel() {
    219        return getOffering().getModel();
    220    }
    221
    222    /**
    223     * Enrollment weight -- weight of all requests that are enrolled into this course,
    224     * excluding the given one. See
    225     * {@link Request#getWeight()}.
    226     * @param assignment current assignment
    227     * @param excludeRequest request to exclude
    228     * @return enrollment weight
    229     */
    230    public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
    231        return getContext(assignment).getEnrollmentWeight(assignment, excludeRequest);
    232    }
    233
    234    /** Set of assigned enrollments
    235     * @param assignment current assignment
    236     * @return assigned enrollments for this course offering
    237     **/
    238    public Set<Enrollment> getEnrollments(Assignment<Request, Enrollment> assignment) {
    239        return getContext(assignment).getEnrollments();
    240    }
    241
    242    /**
    243     * Maximal weight of a single enrollment in the course
    244     * @param assignment current assignment
    245     * @return maximal enrollment weight
    246     */
    247    public double getMaxEnrollmentWeight(Assignment<Request, Enrollment> assignment) {
    248        return getContext(assignment).getMaxEnrollmentWeight();
    249    }
    250
    251    /**
    252     * Minimal weight of a single enrollment in the course
    253     * @param assignment current assignment
    254     * @return minimal enrollment weight
    255     */
    256    public double getMinEnrollmentWeight(Assignment<Request, Enrollment> assignment) {
    257        return getContext(assignment).getMinEnrollmentWeight();
    258    }
    259
    260    /**
    261     * Add request group of this course. This is automatically called
    262     * by the constructor of the {@link RequestGroup}.
    263     * @param group request group to be added
    264     */
    265    public void addRequestGroup(RequestGroup group) {
    266        iRequestGroups.add(group);
    267    }
    268
    269    /**
    270     * Remove request group from this course.
    271     * @param group request group to be removed
    272     */
    273    public void removeRequestGroup(RequestGroup group) {
    274        iRequestGroups.remove(group);
    275    }
    276
    277    /**
    278     * Lists all the request groups of this course
    279     * @return all request groups of this course
    280     */
    281    public Set<RequestGroup> getRequestGroups() {
    282        return iRequestGroups;
    283    }
    284
    285    /**
    286     * Set credit (Online Student Scheduling only)
    287     * @param credit scheduling course credit
    288     */
    289    public void setCredit(String credit) {
    290        iCredit = credit;
    291        if (iCreditValue == null && credit != null) {
    292            int split = credit.indexOf('|');
    293            String abbv = null;
    294            if (split >= 0) {
    295                abbv = credit.substring(0, split);
    296            } else {
    297                abbv = credit;
    298            }
    299            Matcher m = Pattern.compile("(^| )(\\d+\\.?\\d*)([,-]?(\\d+\\.?\\d*))?($| )").matcher(abbv);
    300            if (m.find())
    301                iCreditValue = Float.parseFloat(m.group(2));
    302        }
    303    }
    304
    305    /**
    306     * Get credit (Online Student Scheduling only)
    307     * @return scheduling course credit
    308     */
    309    public String getCredit() { return iCredit; }
    310
    311    /**
    312     * True if this course has a credit value defined
    313     * @return true if a credit value is set
    314     */
    315    public boolean hasCreditValue() { return iCreditValue != null; }
    316
    317    /**
    318     * Set course credit value (null if not set)
    319     * @param creditValue course credit value
    320     */
    321    public void setCreditValue(Float creditValue) { iCreditValue = creditValue; }
    322
    323    /**
    324     * Get course credit value (null if not set)
    325     * return course credit value
    326     */
    327    public Float getCreditValue() { return iCreditValue; }
    328
    329    @Override
    330    public CourseContext createAssignmentContext(Assignment<Request, Enrollment> assignment) {
    331        return new CourseContext(assignment);
    332    }
    333
    334
    335    @Override
    336    public CourseContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, CourseContext parentContext) {
    337        return new CourseContext(parentContext);
    338    }
    339
    340    public class CourseContext implements AssignmentConstraintContext<Request, Enrollment> {
    341        private double iEnrollmentWeight = 0.0;
    342        private Set<Enrollment> iEnrollments = null;
    343        private double iMaxEnrollmentWeight = 0.0;
    344        private double iMinEnrollmentWeight = 0.0;
    345        private boolean iReadOnly = false;
    346
    347        public CourseContext(Assignment<Request, Enrollment> assignment) {
    348            iEnrollments = new HashSet<Enrollment>();
    349            for (CourseRequest request: getRequests()) {
    350                Enrollment enrollment = assignment.getValue(request);
    351                if (enrollment != null && Course.this.equals(enrollment.getCourse()))
    352                    assigned(assignment, enrollment);
    353            }
    354        }
    355
    356        public CourseContext(CourseContext parent) {
    357            iEnrollmentWeight = parent.iEnrollmentWeight;
    358            iMinEnrollmentWeight = parent.iMinEnrollmentWeight;
    359            iMaxEnrollmentWeight = parent.iMaxEnrollmentWeight;
    360            iEnrollments = parent.iEnrollments;
    361            iReadOnly = true;
    362        }
    363
    364        @Override
    365        public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    366            if (iReadOnly) {
    367                iEnrollments = new HashSet<Enrollment>(iEnrollments);
    368                iReadOnly = false;
    369            }
    370            if (iEnrollments.isEmpty()) {
    371                iMinEnrollmentWeight = iMaxEnrollmentWeight = enrollment.getRequest().getWeight();
    372            } else {
    373                iMaxEnrollmentWeight = Math.max(iMaxEnrollmentWeight, enrollment.getRequest().getWeight());
    374                iMinEnrollmentWeight = Math.min(iMinEnrollmentWeight, enrollment.getRequest().getWeight());
    375            }
    376            if (iEnrollments.add(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()))
    377                iEnrollmentWeight += enrollment.getRequest().getWeight();
    378        }
    379
    380        @Override
    381        public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    382            if (iReadOnly) {
    383                iEnrollments = new HashSet<Enrollment>(iEnrollments);
    384                iReadOnly = false;
    385            }
    386            if (iEnrollments.remove(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()))
    387                iEnrollmentWeight -= enrollment.getRequest().getWeight();
    388            if (iEnrollments.isEmpty()) {
    389                iMinEnrollmentWeight = iMaxEnrollmentWeight = 0;
    390            } else if (iMinEnrollmentWeight != iMaxEnrollmentWeight) {
    391                if (iMinEnrollmentWeight == enrollment.getRequest().getWeight()) {
    392                    double newMinEnrollmentWeight = Double.MAX_VALUE;
    393                    for (Enrollment e : iEnrollments) {
    394                        if (e.getRequest().getWeight() == iMinEnrollmentWeight) {
    395                            newMinEnrollmentWeight = iMinEnrollmentWeight;
    396                            break;
    397                        } else {
    398                            newMinEnrollmentWeight = Math.min(newMinEnrollmentWeight, e.getRequest().getWeight());
    399                        }
    400                    }
    401                    iMinEnrollmentWeight = newMinEnrollmentWeight;
    402                }
    403                if (iMaxEnrollmentWeight == enrollment.getRequest().getWeight()) {
    404                    double newMaxEnrollmentWeight = Double.MIN_VALUE;
    405                    for (Enrollment e : iEnrollments) {
    406                        if (e.getRequest().getWeight() == iMaxEnrollmentWeight) {
    407                            newMaxEnrollmentWeight = iMaxEnrollmentWeight;
    408                            break;
    409                        } else {
    410                            newMaxEnrollmentWeight = Math.max(newMaxEnrollmentWeight, e.getRequest().getWeight());
    411                        }
    412                    }
    413                    iMaxEnrollmentWeight = newMaxEnrollmentWeight;
    414                }
    415            }
    416        }
    417
    418        /**
    419         * Enrollment weight -- weight of all requests that are enrolled into this course,
    420         * excluding the given one. See
    421         * {@link Request#getWeight()}.
    422         * @param assignment current assignment
    423         * @param excludeRequest request to exclude
    424         * @return enrollment weight
    425         */
    426        public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
    427            double weight = iEnrollmentWeight;
    428            if (excludeRequest != null) {
    429                Enrollment enrollment = assignment.getValue(excludeRequest);
    430                if (enrollment!= null && iEnrollments.contains(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()))
    431                    weight -= excludeRequest.getWeight();
    432            }
    433            return weight;
    434        }
    435
    436        /** Set of assigned enrollments
    437         * @return assigned enrollments for this course offering
    438         **/
    439        public Set<Enrollment> getEnrollments() {
    440            return iEnrollments;
    441        }
    442
    443        /**
    444         * Maximal weight of a single enrollment in the course
    445         * @return maximal enrollment weight
    446         */
    447        public double getMaxEnrollmentWeight() {
    448            return iMaxEnrollmentWeight;
    449        }
    450
    451        /**
    452         * Minimal weight of a single enrollment in the course
    453         * @return minimal enrollment weight
    454         */
    455        public double getMinEnrollmentWeight() {
    456            return iMinEnrollmentWeight;
    457        }
    458    }
    459
    460    public double getOnlineBound() {
    461        double bound = 1.0;
    462        for (Config config: getOffering().getConfigs()) {
    463            int online = config.getNrOnline();
    464            if (online == 0) return 0.0;
    465            double factor = ((double)online) / config.getSubparts().size();
    466            if (factor < bound) bound = factor;
    467        }
    468        return bound;
    469    }
    470
    471    public double getArrHrsBound() {
    472        double bound = 1.0;
    473        for (Config config: getOffering().getConfigs()) {
    474            int arrHrs = config.getNrArrHours();
    475            if (arrHrs == 0) return 0.0;
    476            double factor = ((double)arrHrs) / config.getSubparts().size();
    477            if (factor < bound) bound = factor;
    478        }
    479        return bound;
    480    }
    481
    482    public double getPastBound() {
    483        double bound = 1.0;
    484        for (Config config: getOffering().getConfigs()) {
    485            int past = config.getNrPast();
    486            if (past == 0) return 0.0;
    487            double factor = ((double)past) / config.getSubparts().size();
    488            if (factor < bound) bound = factor;
    489        }
    490        return bound;
    491    }
    492
    493    /**
    494     * Course title
    495     */
    496    public String getTitle() {
    497        return iTitle;
    498    }
    499
    500    /**
    501     * Course title
    502     */
    503    public void setTitle(String title) {
    504        iTitle = title;
    505    }
    506
    507    /**
    508     * Course type
    509     */
    510    public String getType() {
    511        return iType;
    512    }
    513
    514    /**
    515     * Course type
    516     */
    517    public void setType(String type) {
    518        iType = type;
    519    }
    520
    521    /**
    522     * Parent course is set if the course depends on the parent.
    523     * If the student is requesting both this course and its parent, they cannot get this course without also getting the parent course.
    524     * @return parent offering
    525     */
    526    public Course getParent() { return iParent; }
    527
    528    /**
    529     * Parent course is set if the course depends on the parent.
    530     * If the student is requesting both this course and its parent, they cannot get this course without also getting the parent course.
    531     * @return true if parent is set (this course depends on some other course)
    532     */
    533    public boolean hasParent() { return iParent != null; }
    534
    535    /**
    536     * Return true if this course is a parent of at least one other course.
    537     */
    538    public boolean hasChildren() { return iChildren != null && !iChildren.isEmpty(); }
    539
    540    /**
    541     * Courses that this course is a parent of.
    542     */
    543    public Set<Course> getChildren() { return iChildren; }
    544
    545    /**
    546     * Parent course is set if the course depends on the parent.
    547     * If the student is requesting both this course and its parent, they cannot get this course without also getting the parent course.
    548     * parameter parent parent course, null if this offering has no parent
    549     */
    550    public void setParent(Course parent) {
    551        iParent = parent;
    552        if (parent.iChildren == null) {
    553            parent.iChildren = new HashSet<Course>();
    554            parent.iChildren.add(this);
    555        }
    556    }
    557}