001package org.cpsolver.studentsct.model;
002
003import java.text.DecimalFormat;
004import java.util.HashSet;
005import java.util.Iterator;
006import java.util.Set;
007
008import org.cpsolver.ifs.assignment.Assignment;
009import org.cpsolver.ifs.model.Value;
010import org.cpsolver.ifs.util.ToolBox;
011import org.cpsolver.studentsct.StudentSectioningModel;
012import org.cpsolver.studentsct.extension.DistanceConflict;
013import org.cpsolver.studentsct.extension.StudentQuality;
014import org.cpsolver.studentsct.extension.TimeOverlapsCounter;
015import org.cpsolver.studentsct.reservation.Reservation;
016
017
018/**
019 * Representation of an enrollment of a student into a course. A student needs
020 * to be enrolled in a section of each subpart of a selected configuration. When
021 * parent-child relation is defined among sections, if a student is enrolled in
022 * a section that has a parent section defined, he/she has be enrolled in the
023 * parent section as well. Also, the selected sections cannot overlap in time. <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 */
045
046public class Enrollment extends Value<Request, Enrollment> {
047    private static DecimalFormat sDF = new DecimalFormat("0.000");
048    private Request iRequest = null;
049    private Config iConfig = null;
050    private Course iCourse = null;
051    private Set<? extends SctAssignment> iAssignments = null;
052    private Double iCachedPenalty = null;
053    private int iPriority = 0;
054    private boolean iNoReservationPenalty = false;
055    private Reservation iReservation = null;
056    private Long iTimeStamp = null;
057    private String iApproval = null;
058
059    /**
060     * Constructor
061     * 
062     * @param request
063     *            course / free time request
064     * @param priority
065     *            zero for the course, one for the first alternative, two for the second alternative
066     * @param noReservationPenalty
067     *            when true +1 is added to priority (prefer enrollments with reservations)
068     * @param course
069     *            selected course
070     * @param config
071     *            selected configuration
072     * @param assignments
073     *            valid list of sections
074     * @param reservation used reservation
075     */
076    public Enrollment(Request request, int priority, boolean noReservationPenalty, Course course, Config config, Set<? extends SctAssignment> assignments, Reservation reservation) {
077        super(request);
078        iRequest = request;
079        iConfig = config;
080        iAssignments = assignments;
081        iPriority = priority;
082        iCourse = course;
083        iNoReservationPenalty = noReservationPenalty;
084        if (iConfig != null && iCourse == null)
085            for (Course c: ((CourseRequest)iRequest).getCourses()) {
086                if (c.getOffering().getConfigs().contains(iConfig)) {
087                    iCourse = c;
088                    break;
089                }
090            }
091        iReservation = reservation;
092    }
093    
094    /**
095     * Constructor
096     * 
097     * @param request
098     *            course / free time request
099     * @param priority
100     *            zero for the course, one for the first alternative, two for the second alternative
101     * @param course
102     *            selected course
103     * @param config
104     *            selected configuration
105     * @param assignments
106     *            valid list of sections
107     * @param reservation used reservation
108     */
109    public Enrollment(Request request, int priority, Course course, Config config, Set<? extends SctAssignment> assignments, Reservation reservation) {
110        this(request, priority, false, course, config, assignments, reservation);
111    }
112    
113    /**
114     * Constructor
115     * 
116     * @param request
117     *            course / free time request
118     * @param priority
119     *            zero for the course, one for the first alternative, two for the second alternative
120     * @param config
121     *            selected configuration
122     * @param assignments
123     *            valid list of sections
124     * @param assignment current assignment (to guess the reservation)
125     */
126    public Enrollment(Request request, int priority, Config config, Set<? extends SctAssignment> assignments, Assignment<Request, Enrollment> assignment) {
127        this(request, priority, null, config, assignments, null);
128        if (assignments != null && assignment != null)
129            guessReservation(assignment, true);
130    }
131    
132    /**
133     * Guess the reservation based on the enrollment
134     * @param assignment current assignment
135     * @param onlyAvailable use only reservation that have some space left in them
136     */
137    public void guessReservation(Assignment<Request, Enrollment> assignment, boolean onlyAvailable) {
138        if (iCourse != null) {
139            Reservation best = null;
140            for (Reservation reservation: ((CourseRequest)iRequest).getReservations(iCourse)) {
141                if (reservation.isIncluded(this)) {
142                    if (onlyAvailable && reservation.getContext(assignment).getReservedAvailableSpace(assignment, iConfig, iRequest) < iRequest.getWeight() && !reservation.canBatchAssignOverLimit())
143                        continue;
144                    if (best == null || best.getPriority() > reservation.getPriority()) {
145                        best = reservation;
146                    } else if (best.getPriority() == reservation.getPriority() &&
147                        best.getContext(assignment).getReservedAvailableSpace(assignment, iConfig, iRequest) < reservation.getContext(assignment).getReservedAvailableSpace(assignment, iConfig, iRequest)) {
148                        best = reservation;
149                    }
150                }
151            }
152            iReservation = best;
153        }
154    }
155    
156    /** Student 
157     * @return student
158     **/
159    public Student getStudent() {
160        return iRequest.getStudent();
161    }
162
163    /** Request 
164     * @return request
165     **/
166    public Request getRequest() {
167        return iRequest;
168    }
169
170    /** True if the request is course request 
171     * @return true if the request if course request
172     **/
173    public boolean isCourseRequest() {
174        return iConfig != null;
175    }
176
177    /** Offering of the course request 
178     * @return offering of the course request
179     **/
180    public Offering getOffering() {
181        return (iConfig == null ? null : iConfig.getOffering());
182    }
183
184    /** Config of the course request 
185     * @return config of the course request
186     **/
187    public Config getConfig() {
188        return iConfig;
189    }
190    
191    /** Course of the course request 
192     * @return course of the course request
193     **/
194    public Course getCourse() {
195        return iCourse;
196    }
197
198    /** List of assignments (selected sections) 
199     * @return assignments (selected sections)
200     **/
201    @SuppressWarnings("unchecked")
202    public Set<SctAssignment> getAssignments() {
203        return (Set<SctAssignment>) iAssignments;
204    }
205
206    /** List of sections (only for course request) 
207     * @return selected sections
208     **/
209    @SuppressWarnings("unchecked")
210    public Set<Section> getSections() {
211        if (isCourseRequest())
212            return (Set<Section>) iAssignments;
213        return new HashSet<Section>();
214    }
215
216    /** True when this enrollment is overlapping with the given enrollment 
217     * @param enrl other enrollment
218     * @return true if there is an overlap 
219     **/
220    public boolean isOverlapping(Enrollment enrl) {
221        if (enrl == null || isAllowOverlap() || enrl.isAllowOverlap())
222            return false;
223        for (SctAssignment a : getAssignments()) {
224            if (a.isOverlapping(enrl.getAssignments()))
225                return true;
226        }
227        return false;
228    }
229
230    /** Percent of sections that are wait-listed 
231     * @return percent of sections that are wait-listed
232     **/
233    public double percentWaitlisted() {
234        if (!isCourseRequest())
235            return 0.0;
236        CourseRequest courseRequest = (CourseRequest) getRequest();
237        int nrWaitlisted = 0;
238        for (Section section : getSections()) {
239            if (courseRequest.isWaitlisted(section))
240                nrWaitlisted++;
241        }
242        return ((double) nrWaitlisted) / getAssignments().size();
243    }
244
245    /** Percent of sections that are selected 
246     * @return percent of sections that are selected
247     **/
248    public double percentSelected() {
249        if (!isCourseRequest())
250            return 0.0;
251        CourseRequest courseRequest = (CourseRequest) getRequest();
252        int nrSelected = 0;
253        for (Section section : getSections()) {
254            if (courseRequest.isSelected(section))
255                nrSelected++;
256        }
257        return ((double) nrSelected) / getAssignments().size();
258    }
259    
260    /** Percent of sections that are selected 
261     * @return percent of sections that are selected
262     **/
263    public double percentSelectedSameSection() {
264        if (!isCourseRequest() || getStudent().isDummy()) return (getRequest().hasSelection() ? 1.0 : 0.0);
265        CourseRequest courseRequest = (CourseRequest) getRequest();
266        int nrSelected = 0;
267        Set<Long> nrMatching = new HashSet<Long>();
268        sections: for (Section section : getSections()) {
269            for (Choice choice: courseRequest.getSelectedChoices()) {
270                if (choice.getSubpartId() != null) nrMatching.add(choice.getSubpartId());
271                if (choice.sameSection(section)) {
272                    nrSelected ++; continue sections;
273                }
274            }
275        }
276        return (nrMatching.isEmpty() ? 1.0 : ((double) nrSelected) / nrMatching.size());
277    }
278    
279    /** Percent of sections that have the same configuration 
280     * @return percent of sections that are selected
281     **/
282    public double percentSelectedSameConfig() {
283        if (!isCourseRequest() || getStudent().isDummy() || getConfig() == null) return (getRequest().hasSelection() ? 1.0 : 0.0);
284        CourseRequest courseRequest = (CourseRequest) getRequest();
285        boolean hasConfigSelection = false;
286        for (Choice choice: courseRequest.getSelectedChoices()) {
287            if (choice.getConfigId() != null) {
288                hasConfigSelection = true;
289                if (choice.getConfigId().equals(getConfig().getId())) return 1.0;
290            }
291        }
292        return (hasConfigSelection ? 0.0 : 1.0);
293    }
294
295    /** Percent of sections that are initial 
296     * @return percent of sections that of the initial enrollment
297     **/
298    public double percentInitial() {
299        if (!isCourseRequest())
300            return 0.0;
301        if (getRequest().getInitialAssignment() == null)
302            return 0.0;
303        Enrollment inital = getRequest().getInitialAssignment();
304        int nrInitial = 0;
305        for (Section section : getSections()) {
306            if (inital.getAssignments().contains(section))
307                nrInitial++;
308        }
309        return ((double) nrInitial) / getAssignments().size();
310    }
311    
312    /** Percent of sections that have same time as the initial assignment 
313     * @return percent of sections that have same time as the initial assignment
314     **/
315    public double percentSameTime() {
316        if (!isCourseRequest())
317            return 0.0;
318        Enrollment ie = getRequest().getInitialAssignment();
319        if (ie != null) {
320            int nrInitial = 0;
321            sections: for (Section section : getSections()) {
322                for (Section initial: ie.getSections()) {
323                    if (section.sameInstructionalType(initial) && section.sameTime(initial)) {
324                        nrInitial ++;
325                        continue sections;
326                    }
327                }
328            }
329            return ((double) nrInitial) / getAssignments().size();
330        }
331        Set<Choice> selected = ((CourseRequest)getRequest()).getSelectedChoices();
332        if (!selected.isEmpty()) {
333            int nrInitial = 0;
334            sections: for (Section section : getSections()) {
335                for (Choice choice: selected) {
336                    if (choice.sameOffering(section) && choice.sameInstructionalType(section) && choice.sameTime(section)) {
337                        nrInitial ++;
338                        continue sections;
339                    }
340                    
341                }
342            }
343            return ((double) nrInitial) / getAssignments().size();
344        }
345        return 0.0;
346    }
347
348    /** True if all the sections are wait-listed 
349     * @return all the sections are wait-listed 
350     **/
351    public boolean isWaitlisted() {
352        if (!isCourseRequest())
353            return false;
354        CourseRequest courseRequest = (CourseRequest) getRequest();
355        for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
356            Section section = (Section) i.next();
357            if (!courseRequest.isWaitlisted(section))
358                return false;
359        }
360        return true;
361    }
362
363    /** True if all the sections are selected 
364     * @return all the sections are selected
365     **/
366    public boolean isSelected() {
367        if (!isCourseRequest())
368            return false;
369        CourseRequest courseRequest = (CourseRequest) getRequest();
370        for (Section section : getSections()) {
371            if (!courseRequest.isSelected(section))
372                return false;
373        }
374        return true;
375    }
376    
377    public boolean isRequired() {
378        if (!isCourseRequest())
379            return false;
380        CourseRequest courseRequest = (CourseRequest) getRequest();
381        for (Section section : getSections()) {
382            if (!courseRequest.isRequired(section))
383                return false;
384        }
385        return true;
386    }
387
388    /**
389     * Enrollment penalty -- sum of section penalties (see
390     * {@link Section#getPenalty()})
391     * @return online penalty
392     */
393    public double getPenalty() {
394        if (iCachedPenalty == null) {
395            double penalty = 0.0;
396            if (isCourseRequest()) {
397                for (Section section : getSections()) {
398                    penalty += section.getPenalty();
399                }
400            }
401            iCachedPenalty = Double.valueOf(penalty / getAssignments().size());
402        }
403        return iCachedPenalty.doubleValue();
404    }
405
406    /** Enrollment value */
407    @Override
408    public double toDouble(Assignment<Request, Enrollment> assignment) {
409        return toDouble(assignment, true);
410    }
411    
412    /** Enrollment value
413     * @param assignment current assignment
414     * @param precise if false, distance conflicts and time overlaps are ignored (i.e., much faster, but less precise computation)
415     * @return enrollment penalty
416     **/
417    public double toDouble(Assignment<Request, Enrollment> assignment, boolean precise) {
418        if (precise) {
419            StudentSectioningModel model = (StudentSectioningModel)variable().getModel();
420            if (model.getStudentQuality() != null)
421                return - getRequest().getWeight() * model.getStudentWeights().getWeight(assignment, this, studentQualityConflicts(assignment));
422            else
423                return - getRequest().getWeight() * model.getStudentWeights().getWeight(assignment, this, distanceConflicts(assignment), timeOverlappingConflicts(assignment));
424        } else {
425            Double value = (assignment == null ? null : variable().getContext(assignment).getLastWeight());
426            if (value != null) return - value;
427            return - getRequest().getWeight() * ((StudentSectioningModel)variable().getModel()).getStudentWeights().getWeight(assignment, this);
428        }
429    }
430    
431    /** Enrollment name */
432    @Override
433    public String getName() {
434        if (getRequest() instanceof CourseRequest) {
435            Course course = null;
436            CourseRequest courseRequest = (CourseRequest) getRequest();
437            for (Course c : courseRequest.getCourses()) {
438                if (c.getOffering().getConfigs().contains(getConfig())) {
439                    course = c;
440                    break;
441                }
442            }
443            String ret = (course == null ? getConfig() == null ? "" : getConfig().getName() : course.getName());
444            for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
445                Section assignment = (Section) i.next();
446                ret += "\n  " + assignment.getLongName(true) + (i.hasNext() ? "," : "");
447            }
448            return ret;
449        } else if (getRequest() instanceof FreeTimeRequest) {
450            return "Free Time " + ((FreeTimeRequest) getRequest()).getTime().getLongName(true);
451        } else {
452            String ret = "";
453            for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
454                SctAssignment assignment = i.next();
455                ret += assignment.toString() + (i.hasNext() ? "," : "");
456                if (i.hasNext())
457                    ret += "\n  ";
458            }
459            return ret;
460        }
461    }
462
463    public String toString(Assignment<Request, Enrollment> a) {
464        if (getAssignments().isEmpty()) return "not assigned";
465        Set<DistanceConflict.Conflict> dc = distanceConflicts(a);
466        Set<TimeOverlapsCounter.Conflict> toc = timeOverlappingConflicts(a);
467        int share = 0;
468        if (toc != null)
469            for (TimeOverlapsCounter.Conflict c: toc)
470                share += c.getShare();
471        String ret = toDouble(a) + "/" + sDF.format(getRequest().getBound())
472                + (getPenalty() == 0.0 ? "" : "/" + sDF.format(getPenalty()))
473                + (dc == null || dc.isEmpty() ? "" : "/dc:" + dc.size())
474                + (share <= 0 ? "" : "/toc:" + share);
475        if (getRequest() instanceof CourseRequest) {
476            double sameGroup = 0.0; int groupCount = 0;
477            for (RequestGroup g: ((CourseRequest)getRequest()).getRequestGroups()) {
478                if (g.getCourse().equals(getCourse())) {
479                    sameGroup += g.getEnrollmentSpread(a, this, 1.0, 0.0);
480                    groupCount ++;
481                }
482            }
483            if (groupCount > 0)
484                ret += "/g:" + sDF.format(sameGroup / groupCount);
485        }
486        if (getRequest() instanceof CourseRequest) {
487            ret += " ";
488            for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
489                SctAssignment assignment = i.next();
490                ret += assignment + (i.hasNext() ? ", " : "");
491            }
492        }
493        if (getReservation() != null) ret = "(r) " + ret;
494        return ret;
495    }
496    
497    @Override
498    public String toString() {
499        if (getAssignments().isEmpty()) return "not assigned";
500        String ret = sDF.format(getRequest().getBound()) + (getPenalty() == 0.0 ? "" : "/" + sDF.format(getPenalty()));
501        if (getRequest() instanceof CourseRequest) {
502            ret += " ";
503            for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
504                SctAssignment assignment = i.next();
505                ret += assignment + (i.hasNext() ? ", " : "");
506            }
507        }
508        if (getReservation() != null) ret = "(r) " + ret;
509        if (getRequest().isMPP()) ret += " [i" + sDF.format(100.0 * percentInitial()) + "/t" + sDF.format(100.0 * percentSameTime()) + "]";
510        return ret;
511    }
512
513    @Override
514    public boolean equals(Object o) {
515        if (o == null || !(o instanceof Enrollment))
516            return false;
517        Enrollment e = (Enrollment) o;
518        if (!ToolBox.equals(getCourse(), e.getCourse()))
519            return false;
520        if (!ToolBox.equals(getConfig(), e.getConfig()))
521            return false;
522        if (!ToolBox.equals(getRequest(), e.getRequest()))
523            return false;
524        if (!ToolBox.equals(getAssignments(), e.getAssignments()))
525            return false;
526        if (!ToolBox.equals(getReservation(), e.getReservation()))
527            return false;
528        return true;
529    }
530
531    /** Distance conflicts, in which this enrollment is involved. 
532     * @param assignment current assignment
533     * @return distance conflicts
534     **/
535    public Set<DistanceConflict.Conflict> distanceConflicts(Assignment<Request, Enrollment> assignment) {
536        if (!isCourseRequest())
537            return null;
538        if (getRequest().getModel() instanceof StudentSectioningModel) {
539            DistanceConflict dc = ((StudentSectioningModel) getRequest().getModel()).getDistanceConflict();
540            if (dc == null) return null;
541            return dc.allConflicts(assignment, this);
542        } else
543            return null;
544    }
545
546    /** Time overlapping conflicts, in which this enrollment is involved. 
547     * @param assignment current assignment
548     * @return time overlapping conflicts
549     **/
550    public Set<TimeOverlapsCounter.Conflict> timeOverlappingConflicts(Assignment<Request, Enrollment> assignment) {
551        if (getRequest().getModel() instanceof StudentSectioningModel) {
552            TimeOverlapsCounter toc = ((StudentSectioningModel) getRequest().getModel()).getTimeOverlaps();
553            if (toc == null)
554                return null;
555            return toc.allConflicts(assignment, this);
556        } else
557            return null;
558    }
559    
560    public Set<StudentQuality.Conflict> studentQualityConflicts(Assignment<Request, Enrollment> assignment) {
561        if (!isCourseRequest())
562            return null;
563        if (getRequest().getModel() instanceof StudentSectioningModel) {
564            StudentQuality sq = ((StudentSectioningModel) getRequest().getModel()).getStudentQuality();
565            if (sq == null) return null;
566            return sq.allConflicts(assignment, this);
567        } else
568            return null;
569    }
570
571    /** 
572     * Return enrollment priority
573     * @return zero for the course, one for the first alternative, two for the second alternative
574     */
575    public int getPriority() {
576        return iPriority + (iNoReservationPenalty ? 1 : 0);
577    }
578    
579    /** 
580     * Return enrollment priority, ignoring priority bump provided by reservations
581     * @return zero for the course, one for the first alternative, two for the second alternative
582     */
583    public int getTruePriority() {
584        return iPriority;
585    }
586    
587    /** 
588     * Return adjusted enrollment priority, including priority bump provided by reservations
589     * (but ensuring that getting the course without a reservation is still better than getting an alternative) 
590     * @return zero for the course, two for the first alternative, four for the second alternative; plus one when the no reservation penalty applies
591     */
592    public int getAdjustedPriority() {
593        return 2 * iPriority + (iNoReservationPenalty ? 1 : 0);
594    }
595    
596    /**
597     * Return total number of slots of all sections in the enrollment.
598     * @return number of slots used
599     */
600    public int getNrSlots() {
601        int ret = 0;
602        for (SctAssignment a: getAssignments()) {
603            if (a.getTime() != null) ret += a.getTime().getLength() * a.getTime().getNrMeetings();
604        }
605        return ret;
606    }
607    
608    /**
609     * Return reservation used for this enrollment
610     * @return used reservation
611     */
612    public Reservation getReservation() { return iReservation; }
613    
614    /**
615     * Set reservation for this enrollment
616     * @param reservation used reservation
617     */
618    public void setReservation(Reservation reservation) { iReservation = reservation; }
619    
620    /**
621     * Time stamp of the enrollment
622     * @return enrollment time stamp
623     */
624    public Long getTimeStamp() {
625        return iTimeStamp;
626    }
627
628    /**
629     * Time stamp of the enrollment
630     * @param timeStamp enrollment time stamp
631     */
632    public void setTimeStamp(Long timeStamp) {
633        iTimeStamp = timeStamp;
634    }
635
636    /**
637     * Approval of the enrollment (only used by the online student sectioning)
638     * @return consent approval
639     */
640    public String getApproval() {
641        return iApproval;
642    }
643
644    /**
645     * Approval of the enrollment (only used by the online student sectioning)
646     * @param approval consent approval
647     */
648    public void setApproval(String approval) {
649        iApproval = approval;
650    }
651    
652    /**
653     * True if this enrollment can overlap with other enrollments of the student.
654     * @return can overlap with other enrollments of the student
655     */
656    public boolean isAllowOverlap() {
657        return (getReservation() != null && getReservation().isAllowOverlap());
658    }
659    
660    /**
661     * Enrollment limit, i.e., the number of students that would be able to get into the offering using this enrollment (if all the sections are empty)  
662     * @return enrollment limit
663     */
664    public int getLimit() {
665        if (!isCourseRequest()) return -1; // free time requests have no limit
666        Integer limit = null;
667        for (Section section: getSections())
668            if (section.getLimit() >= 0) {
669                if (limit == null)
670                    limit = section.getLimit();
671                else
672                    limit = Math.min(limit, section.getLimit());
673            }
674        return (limit == null ? -1 : limit);
675    }
676    
677    /**
678     * Credit of this enrollment (using either {@link Course#getCreditValue()} or {@link Subpart#getCreditValue()} when course credit is not present)
679     * @return credit of this enrollment
680     */
681    public float getCredit() {
682        if (getCourse() == null) return 0f;
683        if (getAssignments().isEmpty()) return 0f;
684        if (getCourse().hasCreditValue()) return getCourse().getCreditValue();
685        float subpartCredit = 0f;
686        for (Subpart subpart: getConfig().getSubparts()) {
687            if (subpart.hasCreditValue()) subpartCredit += subpart.getCreditValue();
688        }
689        return subpartCredit;
690    }
691    
692}