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        return ret;
512    }
513
514    @Override
515    public boolean equals(Object o) {
516        if (o == null || !(o instanceof Enrollment))
517            return false;
518        Enrollment e = (Enrollment) o;
519        if (!ToolBox.equals(getCourse(), e.getCourse()))
520            return false;
521        if (!ToolBox.equals(getConfig(), e.getConfig()))
522            return false;
523        if (!ToolBox.equals(getRequest(), e.getRequest()))
524            return false;
525        if (!ToolBox.equals(getAssignments(), e.getAssignments()))
526            return false;
527        if (!ToolBox.equals(getReservation(), e.getReservation()))
528            return false;
529        return true;
530    }
531
532    /** Distance conflicts, in which this enrollment is involved. 
533     * @param assignment current assignment
534     * @return distance conflicts
535     **/
536    public Set<DistanceConflict.Conflict> distanceConflicts(Assignment<Request, Enrollment> assignment) {
537        if (!isCourseRequest())
538            return null;
539        if (getRequest().getModel() instanceof StudentSectioningModel) {
540            DistanceConflict dc = ((StudentSectioningModel) getRequest().getModel()).getDistanceConflict();
541            if (dc == null) return null;
542            return dc.allConflicts(assignment, this);
543        } else
544            return null;
545    }
546
547    /** Time overlapping conflicts, in which this enrollment is involved. 
548     * @param assignment current assignment
549     * @return time overlapping conflicts
550     **/
551    public Set<TimeOverlapsCounter.Conflict> timeOverlappingConflicts(Assignment<Request, Enrollment> assignment) {
552        if (getRequest().getModel() instanceof StudentSectioningModel) {
553            TimeOverlapsCounter toc = ((StudentSectioningModel) getRequest().getModel()).getTimeOverlaps();
554            if (toc == null)
555                return null;
556            return toc.allConflicts(assignment, this);
557        } else
558            return null;
559    }
560    
561    public Set<StudentQuality.Conflict> studentQualityConflicts(Assignment<Request, Enrollment> assignment) {
562        if (!isCourseRequest())
563            return null;
564        if (getRequest().getModel() instanceof StudentSectioningModel) {
565            StudentQuality sq = ((StudentSectioningModel) getRequest().getModel()).getStudentQuality();
566            if (sq == null) return null;
567            return sq.allConflicts(assignment, this);
568        } else
569            return null;
570    }
571
572    /** 
573     * Return enrollment priority
574     * @return zero for the course, one for the first alternative, two for the second alternative
575     */
576    public int getPriority() {
577        return iPriority + (iNoReservationPenalty ? 1 : 0);
578    }
579    
580    /** 
581     * Return enrollment priority, ignoring priority bump provided by reservations
582     * @return zero for the course, one for the first alternative, two for the second alternative
583     */
584    public int getTruePriority() {
585        return iPriority;
586    }
587    
588    /** 
589     * Return adjusted enrollment priority, including priority bump provided by reservations
590     * (but ensuring that getting the course without a reservation is still better than getting an alternative) 
591     * @return zero for the course, two for the first alternative, four for the second alternative; plus one when the no reservation penalty applies
592     */
593    public int getAdjustedPriority() {
594        return 2 * iPriority + (iNoReservationPenalty ? 1 : 0);
595    }
596    
597    /**
598     * Return total number of slots of all sections in the enrollment.
599     * @return number of slots used
600     */
601    public int getNrSlots() {
602        int ret = 0;
603        for (SctAssignment a: getAssignments()) {
604            if (a.getTime() != null) ret += a.getTime().getLength() * a.getTime().getNrMeetings();
605        }
606        return ret;
607    }
608    
609    /**
610     * Return reservation used for this enrollment
611     * @return used reservation
612     */
613    public Reservation getReservation() { return iReservation; }
614    
615    /**
616     * Set reservation for this enrollment
617     * @param reservation used reservation
618     */
619    public void setReservation(Reservation reservation) { iReservation = reservation; }
620    
621    /**
622     * Time stamp of the enrollment
623     * @return enrollment time stamp
624     */
625    public Long getTimeStamp() {
626        return iTimeStamp;
627    }
628
629    /**
630     * Time stamp of the enrollment
631     * @param timeStamp enrollment time stamp
632     */
633    public void setTimeStamp(Long timeStamp) {
634        iTimeStamp = timeStamp;
635    }
636
637    /**
638     * Approval of the enrollment (only used by the online student sectioning)
639     * @return consent approval
640     */
641    public String getApproval() {
642        return iApproval;
643    }
644
645    /**
646     * Approval of the enrollment (only used by the online student sectioning)
647     * @param approval consent approval
648     */
649    public void setApproval(String approval) {
650        iApproval = approval;
651    }
652    
653    /**
654     * True if this enrollment can overlap with other enrollments of the student.
655     * @return can overlap with other enrollments of the student
656     */
657    public boolean isAllowOverlap() {
658        return (getReservation() != null && getReservation().isAllowOverlap());
659    }
660    
661    /**
662     * 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)  
663     * @return enrollment limit
664     */
665    public int getLimit() {
666        if (!isCourseRequest()) return -1; // free time requests have no limit
667        Integer limit = null;
668        for (Section section: getSections())
669            if (section.getLimit() >= 0) {
670                if (limit == null)
671                    limit = section.getLimit();
672                else
673                    limit = Math.min(limit, section.getLimit());
674            }
675        return (limit == null ? -1 : limit);
676    }
677    
678    /**
679     * Credit of this enrollment (using either {@link Course#getCreditValue()} or {@link Subpart#getCreditValue()} when course credit is not present)
680     * @return credit of this enrollment
681     */
682    public float getCredit() {
683        if (getCourse() == null) return 0f;
684        if (getAssignments().isEmpty()) return 0f;
685        if (getCourse().hasCreditValue()) return getCourse().getCreditValue();
686        float subpartCredit = 0f;
687        for (Subpart subpart: getConfig().getSubparts()) {
688            if (subpart.hasCreditValue()) subpartCredit += subpart.getCreditValue();
689        }
690        return subpartCredit;
691    }
692    
693}