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}