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