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, 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, iRequest) < reservation.getContext(assignment).getReservedAvailableSpace(assignment, 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    /**
378     * Enrollment penalty -- sum of section penalties (see
379     * {@link Section#getPenalty()})
380     * @return online penalty
381     */
382    public double getPenalty() {
383        if (iCachedPenalty == null) {
384            double penalty = 0.0;
385            if (isCourseRequest()) {
386                for (Section section : getSections()) {
387                    penalty += section.getPenalty();
388                }
389            }
390            iCachedPenalty = new Double(penalty / getAssignments().size());
391        }
392        return iCachedPenalty.doubleValue();
393    }
394
395    /** Enrollment value */
396    @Override
397    public double toDouble(Assignment<Request, Enrollment> assignment) {
398        return toDouble(assignment, true);
399    }
400    
401    /** Enrollment value
402     * @param assignment current assignment
403     * @param precise if false, distance conflicts and time overlaps are ignored (i.e., much faster, but less precise computation)
404     * @return enrollment penalty
405     **/
406    public double toDouble(Assignment<Request, Enrollment> assignment, boolean precise) {
407        if (precise) {
408            StudentSectioningModel model = (StudentSectioningModel)variable().getModel();
409            if (model.getStudentQuality() != null)
410                return - getRequest().getWeight() * model.getStudentWeights().getWeight(assignment, this, studentQualityConflicts(assignment));
411            else
412                return - getRequest().getWeight() * model.getStudentWeights().getWeight(assignment, this, distanceConflicts(assignment), timeOverlappingConflicts(assignment));
413        } else {
414            Double value = (assignment == null ? null : variable().getContext(assignment).getLastWeight());
415            if (value != null) return - value;
416            return - getRequest().getWeight() * ((StudentSectioningModel)variable().getModel()).getStudentWeights().getWeight(assignment, this);
417        }
418    }
419    
420    /** Enrollment name */
421    @Override
422    public String getName() {
423        if (getRequest() instanceof CourseRequest) {
424            Course course = null;
425            CourseRequest courseRequest = (CourseRequest) getRequest();
426            for (Course c : courseRequest.getCourses()) {
427                if (c.getOffering().getConfigs().contains(getConfig())) {
428                    course = c;
429                    break;
430                }
431            }
432            String ret = (course == null ? getConfig() == null ? "" : getConfig().getName() : course.getName());
433            for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
434                Section assignment = (Section) i.next();
435                ret += "\n  " + assignment.getLongName(true) + (i.hasNext() ? "," : "");
436            }
437            return ret;
438        } else if (getRequest() instanceof FreeTimeRequest) {
439            return "Free Time " + ((FreeTimeRequest) getRequest()).getTime().getLongName(true);
440        } else {
441            String ret = "";
442            for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
443                SctAssignment assignment = i.next();
444                ret += assignment.toString() + (i.hasNext() ? "," : "");
445                if (i.hasNext())
446                    ret += "\n  ";
447            }
448            return ret;
449        }
450    }
451
452    public String toString(Assignment<Request, Enrollment> a) {
453        if (getAssignments().isEmpty()) return "not assigned";
454        Set<DistanceConflict.Conflict> dc = distanceConflicts(a);
455        Set<TimeOverlapsCounter.Conflict> toc = timeOverlappingConflicts(a);
456        int share = 0;
457        if (toc != null)
458            for (TimeOverlapsCounter.Conflict c: toc)
459                share += c.getShare();
460        String ret = toDouble(a) + "/" + sDF.format(getRequest().getBound())
461                + (getPenalty() == 0.0 ? "" : "/" + sDF.format(getPenalty()))
462                + (dc == null || dc.isEmpty() ? "" : "/dc:" + dc.size())
463                + (share <= 0 ? "" : "/toc:" + share);
464        if (getRequest() instanceof CourseRequest) {
465            double sameGroup = 0.0; int groupCount = 0;
466            for (RequestGroup g: ((CourseRequest)getRequest()).getRequestGroups()) {
467                if (g.getCourse().equals(getCourse())) {
468                    sameGroup += g.getEnrollmentSpread(a, this, 1.0, 0.0);
469                    groupCount ++;
470                }
471            }
472            if (groupCount > 0)
473                ret += "/g:" + sDF.format(sameGroup / groupCount);
474        }
475        if (getRequest() instanceof CourseRequest) {
476            ret += " ";
477            for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
478                SctAssignment assignment = i.next();
479                ret += assignment + (i.hasNext() ? ", " : "");
480            }
481        }
482        if (getReservation() != null) ret = "(r) " + ret;
483        return ret;
484    }
485    
486    @Override
487    public String toString() {
488        if (getAssignments().isEmpty()) return "not assigned";
489        String ret = sDF.format(getRequest().getBound()) + (getPenalty() == 0.0 ? "" : "/" + sDF.format(getPenalty()));
490        if (getRequest() instanceof CourseRequest) {
491            ret += " ";
492            for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
493                SctAssignment assignment = i.next();
494                ret += assignment + (i.hasNext() ? ", " : "");
495            }
496        }
497        if (getReservation() != null) ret = "(r) " + ret;
498        if (getRequest().isMPP()) ret += " [i" + sDF.format(100.0 * percentInitial()) + "/t" + sDF.format(100.0 * percentSameTime()) + "]";
499        return ret;
500    }
501
502    @Override
503    public boolean equals(Object o) {
504        if (o == null || !(o instanceof Enrollment))
505            return false;
506        Enrollment e = (Enrollment) o;
507        if (!ToolBox.equals(getConfig(), e.getConfig()))
508            return false;
509        if (!ToolBox.equals(getRequest(), e.getRequest()))
510            return false;
511        if (!ToolBox.equals(getAssignments(), e.getAssignments()))
512            return false;
513        if (!ToolBox.equals(getReservation(), e.getReservation()))
514            return false;
515        return true;
516    }
517
518    /** Distance conflicts, in which this enrollment is involved. 
519     * @param assignment current assignment
520     * @return distance conflicts
521     **/
522    public Set<DistanceConflict.Conflict> distanceConflicts(Assignment<Request, Enrollment> assignment) {
523        if (!isCourseRequest())
524            return null;
525        if (getRequest().getModel() instanceof StudentSectioningModel) {
526            DistanceConflict dc = ((StudentSectioningModel) getRequest().getModel()).getDistanceConflict();
527            if (dc == null) return null;
528            return dc.allConflicts(assignment, this);
529        } else
530            return null;
531    }
532
533    /** Time overlapping conflicts, in which this enrollment is involved. 
534     * @param assignment current assignment
535     * @return time overlapping conflicts
536     **/
537    public Set<TimeOverlapsCounter.Conflict> timeOverlappingConflicts(Assignment<Request, Enrollment> assignment) {
538        if (getRequest().getModel() instanceof StudentSectioningModel) {
539            TimeOverlapsCounter toc = ((StudentSectioningModel) getRequest().getModel()).getTimeOverlaps();
540            if (toc == null)
541                return null;
542            return toc.allConflicts(assignment, this);
543        } else
544            return null;
545    }
546    
547    public Set<StudentQuality.Conflict> studentQualityConflicts(Assignment<Request, Enrollment> assignment) {
548        if (!isCourseRequest())
549            return null;
550        if (getRequest().getModel() instanceof StudentSectioningModel) {
551            StudentQuality sq = ((StudentSectioningModel) getRequest().getModel()).getStudentQuality();
552            if (sq == null) return null;
553            return sq.allConflicts(assignment, this);
554        } else
555            return null;
556    }
557
558    /** 
559     * Return enrollment priority
560     * @return zero for the course, one for the first alternative, two for the second alternative
561     */
562    public int getPriority() {
563        return iPriority + (iNoReservationPenalty ? 1 : 0);
564    }
565    
566    /** 
567     * Return enrollment priority, ignoring priority bump provided by reservations
568     * @return zero for the course, one for the first alternative, two for the second alternative
569     */
570    public int getTruePriority() {
571        return iPriority;
572    }
573    
574    /** 
575     * Return adjusted enrollment priority, including priority bump provided by reservations
576     * (but ensuring that getting the course without a reservation is still better than getting an alternative) 
577     * @return zero for the course, two for the first alternative, four for the second alternative; plus one when the no reservation penalty applies
578     */
579    public int getAdjustedPriority() {
580        return 2 * iPriority + (iNoReservationPenalty ? 1 : 0);
581    }
582    
583    /**
584     * Return total number of slots of all sections in the enrollment.
585     * @return number of slots used
586     */
587    public int getNrSlots() {
588        int ret = 0;
589        for (SctAssignment a: getAssignments()) {
590            if (a.getTime() != null) ret += a.getTime().getLength() * a.getTime().getNrMeetings();
591        }
592        return ret;
593    }
594    
595    /**
596     * Return reservation used for this enrollment
597     * @return used reservation
598     */
599    public Reservation getReservation() { return iReservation; }
600    
601    /**
602     * Set reservation for this enrollment
603     * @param reservation used reservation
604     */
605    public void setReservation(Reservation reservation) { iReservation = reservation; }
606    
607    /**
608     * Time stamp of the enrollment
609     * @return enrollment time stamp
610     */
611    public Long getTimeStamp() {
612        return iTimeStamp;
613    }
614
615    /**
616     * Time stamp of the enrollment
617     * @param timeStamp enrollment time stamp
618     */
619    public void setTimeStamp(Long timeStamp) {
620        iTimeStamp = timeStamp;
621    }
622
623    /**
624     * Approval of the enrollment (only used by the online student sectioning)
625     * @return consent approval
626     */
627    public String getApproval() {
628        return iApproval;
629    }
630
631    /**
632     * Approval of the enrollment (only used by the online student sectioning)
633     * @param approval consent approval
634     */
635    public void setApproval(String approval) {
636        iApproval = approval;
637    }
638    
639    /**
640     * True if this enrollment can overlap with other enrollments of the student.
641     * @return can overlap with other enrollments of the student
642     */
643    public boolean isAllowOverlap() {
644        return (getReservation() != null && getReservation().isAllowOverlap());
645    }
646    
647    /**
648     * 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)  
649     * @return enrollment limit
650     */
651    public int getLimit() {
652        if (!isCourseRequest()) return -1; // free time requests have no limit
653        Integer limit = null;
654        for (Section section: getSections())
655            if (section.getLimit() >= 0) {
656                if (limit == null)
657                    limit = section.getLimit();
658                else
659                    limit = Math.min(limit, section.getLimit());
660            }
661        return (limit == null ? -1 : limit);
662    }
663    
664    /**
665     * Credit of this enrollment (using either {@link Course#getCreditValue()} or {@link Subpart#getCreditValue()} when course credit is not present)
666     * @return credit of this enrollment
667     */
668    public float getCredit() {
669        if (getCourse() == null) return 0f;
670        if (getAssignments().isEmpty()) return 0f;
671        if (getCourse().hasCreditValue()) return getCourse().getCreditValue();
672        float subpartCredit = 0f;
673        for (Subpart subpart: getConfig().getSubparts()) {
674            if (subpart.hasCreditValue()) subpartCredit += subpart.getCreditValue();
675        }
676        return subpartCredit;
677    }
678    
679}