    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 * @version StudentSct 1.3 (Student Sectioning)<br>
    027 *          Copyright (C) 2007 - 2014 Tomáš Müller<br>
    028 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
    029 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
    030 * <br>
    031 *          This library is free software; you can redistribute it and/or modify
    032 *          it under the terms of the GNU Lesser General Public License as
    033 *          published by the Free Software Foundation; either version 3 of the
    034 *          License, or (at your option) any later version. <br>
    035 * <br>
    036 *          This library is distributed in the hope that it will be useful, but
    037 *          WITHOUT ANY WARRANTY; without even the implied warranty of
    038 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    039 *          Lesser General Public License for more details. <br>
    040 * <br>
    041 *          You should have received a copy of the GNU Lesser General Public
    042 *          License along with this library; if not see
    043 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
    044 */
    045public class Course extends AbstractClassWithContext<Request, Enrollment, Course.CourseContext> implements CanInheritContext<Request, Enrollment, Course.CourseContext> {
    046    private long iId = -1;
    047    private String iSubjectArea = null;
    048    private String iCourseNumber = null;
    049    private Offering iOffering = null;
    050    private int iLimit = 0, iProjected = 0;
    051    private Set<CourseRequest> iRequests = Collections.synchronizedSet(new HashSet<CourseRequest>());
    052    private String iNote = null;
    053    private Set<RequestGroup> iRequestGroups = new HashSet<RequestGroup>();
    054    private String iCredit = null;
    055    private Float iCreditValue = null;
    056
    057    /**
    058     * Constructor
    059     *
    060     * @param id
    061     *            course offering unique id
    062     * @param subjectArea
    063     *            subject area (e.g., MA, CS, ENGL)
    064     * @param courseNumber
    065     *            course number under the given subject area
    066     * @param offering
    067     *            instructional offering which is offered under this course
    068     *            offering
    069     */
    070    public Course(long id, String subjectArea, String courseNumber, Offering offering) {
    071        iId = id;
    072        iSubjectArea = subjectArea;
    073        iCourseNumber = courseNumber;
    074        iOffering = offering;
    075        iOffering.getCourses().add(this);
    076    }
    077
    078    /**
    079     * Constructor
    080     *
    081     * @param id
    082     *            course offering unique id
    083     * @param subjectArea
    084     *            subject area (e.g., MA, CS, ENGL)
    085     * @param courseNumber
    086     *            course number under the given subject area
    087     * @param offering
    088     *            instructional offering which is offered under this course
    089     *            offering
    090     * @param limit
    091     *            course offering limit (-1 for unlimited)
    092     * @param projected
    093     *            projected demand
    094     */
    095    public Course(long id, String subjectArea, String courseNumber, Offering offering, int limit, int projected) {
    096        iId = id;
    097        iSubjectArea = subjectArea;
    098        iCourseNumber = courseNumber;
    099        iOffering = offering;
    100        iOffering.getCourses().add(this);
    101        iLimit = limit;
    102        iProjected = projected;
    103    }
    104
    105    /** Course offering unique id
    106     * @return coure offering unqiue id
    107     **/
    108    public long getId() {
    109        return iId;
    110    }
    111
    112    /** Subject area
    113     * @return subject area abbreviation
    114     **/
    115    public String getSubjectArea() {
    116        return iSubjectArea;
    117    }
    118
    119    /** Course number
    120     * @return course number
    121     **/
    122    public String getCourseNumber() {
    123        return iCourseNumber;
    124    }
    125
    126    /** Course offering name: subject area + course number
    127     * @return course name
    128     **/
    129    public String getName() {
    130        return iSubjectArea + " " + iCourseNumber;
    131    }
    132
    133    @Override
    134    public String toString() {
    135        return getName();
    136    }
    137
    138    /** Instructional offering which is offered under this course offering.
    139     * @return instructional offering
    140     **/
    141    public Offering getOffering() {
    142        return iOffering;
    143    }
    144
    145    /** Course offering limit
    146     * @return course offering limit, -1 if unlimited
    147     **/
    148    public int getLimit() {
    149        return iLimit;
    150    }
    151
    152    /** Set course offering limit
    153     * @param limit course offering limit, -1 if unlimited
    154     **/
    155    public void setLimit(int limit) {
    156        iLimit = limit;
    157    }
    158
    159    /** Course offering projected number of students
    160     * @return course projection
    161     **/
    162    public int getProjected() {
    163        return iProjected;
    164    }
    165
    166    /** Called when an enrollment with this course is assigned to a request
    167     * @param assignment current assignment
    168     * @param enrollment assigned enrollment
    169     **/
    170    public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    171        getContext(assignment).assigned(assignment, enrollment);
    172    }
    173
    174    /** Called when an enrollment with this course is unassigned from a request
    175     * @param assignment current assignment
    176     * @param enrollment unassigned enrollment
    177     */
    178    public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    179        getContext(assignment).unassigned(assignment, enrollment);
    180    }
    181
    182    /** Set of course requests requesting this course
    183     * @return request for this course
    184     **/
    185    public Set<CourseRequest> getRequests() {
    186        return iRequests;
    187    }
    188
    189    /**
    190     * Course note
    191     * @return course note
    192     */
    193    public String getNote() { return iNote; }
    194
    195    /**
    196     * Course note
    197     * @param note course note
    198     */
    199    public void setNote(String note) { iNote = note; }
    200
    201    @Override
    202    public boolean equals(Object o) {
    203        if (o == null || !(o instanceof Course)) return false;
    204        return getId() == ((Course)o).getId();
    205    }
    206
    207    @Override
    208    public int hashCode() {
    209        return (int) (iId ^ (iId >>> 32));
    210    }
    211
    212    @Override
    213    public Model<Request, Enrollment> getModel() {
    214        return getOffering().getModel();
    215    }
    216
    217    /**
    218     * Enrollment weight -- weight of all requests that are enrolled into this course,
    219     * excluding the given one. See
    220     * {@link Request#getWeight()}.
    221     * @param assignment current assignment
    222     * @param excludeRequest request to exclude
    223     * @return enrollment weight
    224     */
    225    public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
    226        return getContext(assignment).getEnrollmentWeight(assignment, excludeRequest);
    227    }
    228
    229    /** Set of assigned enrollments
    230     * @param assignment current assignment
    231     * @return assigned enrollments for this course offering
    232     **/
    233    public Set<Enrollment> getEnrollments(Assignment<Request, Enrollment> assignment) {
    234        return getContext(assignment).getEnrollments();
    235    }
    236
    237    /**
    238     * Maximal weight of a single enrollment in the course
    239     * @param assignment current assignment
    240     * @return maximal enrollment weight
    241     */
    242    public double getMaxEnrollmentWeight(Assignment<Request, Enrollment> assignment) {
    243        return getContext(assignment).getMaxEnrollmentWeight();
    244    }
    245
    246    /**
    247     * Minimal weight of a single enrollment in the course
    248     * @param assignment current assignment
    249     * @return minimal enrollment weight
    250     */
    251    public double getMinEnrollmentWeight(Assignment<Request, Enrollment> assignment) {
    252        return getContext(assignment).getMinEnrollmentWeight();
    253    }
    254
    255    /**
    256     * Add request group of this course. This is automatically called
    257     * by the constructor of the {@link RequestGroup}.
    258     * @param group request group to be added
    259     */
    260    public void addRequestGroup(RequestGroup group) {
    261        iRequestGroups.add(group);
    262    }
    263
    264    /**
    265     * Remove request group from this course.
    266     * @param group request group to be removed
    267     */
    268    public void removeRequestGroup(RequestGroup group) {
    269        iRequestGroups.remove(group);
    270    }
    271
    272    /**
    273     * Lists all the request groups of this course
    274     * @return all request groups of this course
    275     */
    276    public Set<RequestGroup> getRequestGroups() {
    277        return iRequestGroups;
    278    }
    279
    280    /**
    281     * Set credit (Online Student Scheduling only)
    282     * @param credit scheduling course credit
    283     */
    284    public void setCredit(String credit) {
    285        iCredit = credit;
    286        if (iCreditValue == null && credit != null) {
    287            int split = credit.indexOf('|');
    288            String abbv = null;
    289            if (split >= 0) {
    290                abbv = credit.substring(0, split);
    291            } else {
    292                abbv = credit;
    293            }
    294            Matcher m = Pattern.compile("(^| )(\\d+\\.?\\d*)([,-]?(\\d+\\.?\\d*))?($| )").matcher(abbv);
    295            if (m.find())
    296                iCreditValue = Float.parseFloat(m.group(2));
    297        }
    298    }
    299
    300    /**
    301     * Get credit (Online Student Scheduling only)
    302     * @return scheduling course credit
    303     */
    304    public String getCredit() { return iCredit; }
    305
    306    /**
    307     * True if this course has a credit value defined
    308     * @return true if a credit value is set
    309     */
    310    public boolean hasCreditValue() { return iCreditValue != null; }
    311
    312    /**
    313     * Set course credit value (null if not set)
    314     * @param creditValue course credit value
    315     */
    316    public void setCreditValue(Float creditValue) { iCreditValue = creditValue; }
    317
    318    /**
    319     * Get course credit value (null if not set)
    320     * return course credit value
    321     */
    322    public Float getCreditValue() { return iCreditValue; }
    323
    324    @Override
    325    public CourseContext createAssignmentContext(Assignment<Request, Enrollment> assignment) {
    326        return new CourseContext(assignment);
    327    }
    328
    329
    330    @Override
    331    public CourseContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, CourseContext parentContext) {
    332        return new CourseContext(parentContext);
    333    }
    334
    335    public class CourseContext implements AssignmentConstraintContext<Request, Enrollment> {
    336        private double iEnrollmentWeight = 0.0;
    337        private Set<Enrollment> iEnrollments = null;
    338        private double iMaxEnrollmentWeight = 0.0;
    339        private double iMinEnrollmentWeight = 0.0;
    340        private boolean iReadOnly = false;
    341
    342        public CourseContext(Assignment<Request, Enrollment> assignment) {
    343            iEnrollments = new HashSet<Enrollment>();
    344            for (CourseRequest request: getRequests()) {
    345                Enrollment enrollment = assignment.getValue(request);
    346                if (enrollment != null && Course.this.equals(enrollment.getCourse()))
    347                    assigned(assignment, enrollment);
    348            }
    349        }
    350
    351        public CourseContext(CourseContext parent) {
    352            iEnrollmentWeight = parent.iEnrollmentWeight;
    353            iMinEnrollmentWeight = parent.iMinEnrollmentWeight;
    354            iMaxEnrollmentWeight = parent.iMaxEnrollmentWeight;
    355            iEnrollments = parent.iEnrollments;
    356            iReadOnly = true;
    357        }
    358
    359        @Override
    360        public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    361            if (iReadOnly) {
    362                iEnrollments = new HashSet<Enrollment>(iEnrollments);
    363                iReadOnly = false;
    364            }
    365            if (iEnrollments.isEmpty()) {
    366                iMinEnrollmentWeight = iMaxEnrollmentWeight = enrollment.getRequest().getWeight();
    367            } else {
    368                iMaxEnrollmentWeight = Math.max(iMaxEnrollmentWeight, enrollment.getRequest().getWeight());
    369                iMinEnrollmentWeight = Math.min(iMinEnrollmentWeight, enrollment.getRequest().getWeight());
    370            }
    371            if (iEnrollments.add(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()))
    372                iEnrollmentWeight += enrollment.getRequest().getWeight();
    373        }
    374
    375        @Override
    376        public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    377            if (iReadOnly) {
    378                iEnrollments = new HashSet<Enrollment>(iEnrollments);
    379                iReadOnly = false;
    380            }
    381            if (iEnrollments.remove(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()))
    382                iEnrollmentWeight -= enrollment.getRequest().getWeight();
    383            if (iEnrollments.isEmpty()) {
    384                iMinEnrollmentWeight = iMaxEnrollmentWeight = 0;
    385            } else if (iMinEnrollmentWeight != iMaxEnrollmentWeight) {
    386                if (iMinEnrollmentWeight == enrollment.getRequest().getWeight()) {
    387                    double newMinEnrollmentWeight = Double.MAX_VALUE;
    388                    for (Enrollment e : iEnrollments) {
    389                        if (e.getRequest().getWeight() == iMinEnrollmentWeight) {
    390                            newMinEnrollmentWeight = iMinEnrollmentWeight;
    391                            break;
    392                        } else {
    393                            newMinEnrollmentWeight = Math.min(newMinEnrollmentWeight, e.getRequest().getWeight());
    394                        }
    395                    }
    396                    iMinEnrollmentWeight = newMinEnrollmentWeight;
    397                }
    398                if (iMaxEnrollmentWeight == enrollment.getRequest().getWeight()) {
    399                    double newMaxEnrollmentWeight = Double.MIN_VALUE;
    400                    for (Enrollment e : iEnrollments) {
    401                        if (e.getRequest().getWeight() == iMaxEnrollmentWeight) {
    402                            newMaxEnrollmentWeight = iMaxEnrollmentWeight;
    403                            break;
    404                        } else {
    405                            newMaxEnrollmentWeight = Math.max(newMaxEnrollmentWeight, e.getRequest().getWeight());
    406                        }
    407                    }
    408                    iMaxEnrollmentWeight = newMaxEnrollmentWeight;
    409                }
    410            }
    411        }
    412
    413        /**
    414         * Enrollment weight -- weight of all requests that are enrolled into this course,
    415         * excluding the given one. See
    416         * {@link Request#getWeight()}.
    417         * @param assignment current assignment
    418         * @param excludeRequest request to exclude
    419         * @return enrollment weight
    420         */
    421        public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
    422            double weight = iEnrollmentWeight;
    423            if (excludeRequest != null) {
    424                Enrollment enrollment = assignment.getValue(excludeRequest);
    425                if (enrollment!= null && iEnrollments.contains(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()))
    426                    weight -= excludeRequest.getWeight();
    427            }
    428            return weight;
    429        }
    430
    431        /** Set of assigned enrollments
    432         * @return assigned enrollments for this course offering
    433         **/
    434        public Set<Enrollment> getEnrollments() {
    435            return iEnrollments;
    436        }
    437
    438        /**
    439         * Maximal weight of a single enrollment in the course
    440         * @return maximal enrollment weight
    441         */
    442        public double getMaxEnrollmentWeight() {
    443            return iMaxEnrollmentWeight;
    444        }
    445
    446        /**
    447         * Minimal weight of a single enrollment in the course
    448         * @return minimal enrollment weight
    449         */
    450        public double getMinEnrollmentWeight() {
    451            return iMinEnrollmentWeight;
    452        }
    453    }
    454
    455    public double getOnlineBound() {
    456        double bound = 1.0;
    457        for (Config config: getOffering().getConfigs()) {
    458            int online = config.getNrOnline();
    459            if (online == 0) return 0.0;
    460            double factor = ((double)online) / config.getSubparts().size();
    461            if (factor < bound) bound = factor;
    462        }
    463        return bound;
    464    }
    465
    466    public double getArrHrsBound() {
    467        double bound = 1.0;
    468        for (Config config: getOffering().getConfigs()) {
    469            int arrHrs = config.getNrArrHours();
    470            if (arrHrs == 0) return 0.0;
    471            double factor = ((double)arrHrs) / config.getSubparts().size();
    472            if (factor < bound) bound = factor;
    473        }
    474        return bound;
    475    }
    476
    477    public double getPastBound() {
    478        double bound = 1.0;
    479        for (Config config: getOffering().getConfigs()) {
    480            int past = config.getNrPast();
    481            if (past == 0) return 0.0;
    482            double factor = ((double)past) / config.getSubparts().size();
    483            if (factor < bound) bound = factor;
    484        }
    485        return bound;
    486    }
    487}