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}