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.TimeOverlapsCounter;
014import org.cpsolver.studentsct.reservation.Reservation;
015
016
017/**
018 * Representation of an enrollment of a student into a course. A student needs
019 * to be enrolled in a section of each subpart of a selected configuration. When
020 * parent-child relation is defined among sections, if a student is enrolled in
021 * a section that has a parent section defined, he/she has be enrolled in the
022 * parent section as well. Also, the selected sections cannot overlap in time. <br>
023 * <br>
024 * 
025 * @version StudentSct 1.3 (Student Sectioning)<br>
026 *          Copyright (C) 2007 - 2014 Tomas Muller<br>
027 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
028 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
029 * <br>
030 *          This library is free software; you can redistribute it and/or modify
031 *          it under the terms of the GNU Lesser General Public License as
032 *          published by the Free Software Foundation; either version 3 of the
033 *          License, or (at your option) any later version. <br>
034 * <br>
035 *          This library is distributed in the hope that it will be useful, but
036 *          WITHOUT ANY WARRANTY; without even the implied warranty of
037 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
038 *          Lesser General Public License for more details. <br>
039 * <br>
040 *          You should have received a copy of the GNU Lesser General Public
041 *          License along with this library; if not see
042 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
043 */
044
045public class Enrollment extends Value<Request, Enrollment> {
046    private static DecimalFormat sDF = new DecimalFormat("0.000");
047    private Request iRequest = null;
048    private Config iConfig = null;
049    private Course iCourse = null;
050    private Set<? extends SctAssignment> iAssignments = null;
051    private Double iCachedPenalty = null;
052    private int iPriority = 0;
053    private Reservation iReservation = null;
054    private Long iTimeStamp = null;
055    private String iApproval = null;
056
057    /**
058     * Constructor
059     * 
060     * @param request
061     *            course / free time request
062     * @param priority
063     *            zero for the course, one for the first alternative, two for the second alternative
064     * @param course
065     *            selected course
066     * @param config
067     *            selected configuration
068     * @param assignments
069     *            valid list of sections
070     * @param reservation used reservation
071     */
072    public Enrollment(Request request, int priority, Course course, Config config, Set<? extends SctAssignment> assignments, Reservation reservation) {
073        super(request);
074        iRequest = request;
075        iConfig = config;
076        iAssignments = assignments;
077        iPriority = priority;
078        iCourse = course;
079        if (iConfig != null && iCourse == null)
080            for (Course c: ((CourseRequest)iRequest).getCourses()) {
081                if (c.getOffering().getConfigs().contains(iConfig)) {
082                    iCourse = c;
083                    break;
084                }
085            }
086        iReservation = reservation;
087    }
088    
089    /**
090     * Constructor
091     * 
092     * @param request
093     *            course / free time request
094     * @param priority
095     *            zero for the course, one for the first alternative, two for the second alternative
096     * @param config
097     *            selected configuration
098     * @param assignments
099     *            valid list of sections
100     * @param assignment current assignment (to guess the reservation)
101     */
102    public Enrollment(Request request, int priority, Config config, Set<? extends SctAssignment> assignments, Assignment<Request, Enrollment> assignment) {
103        this(request, priority, null, config, assignments, null);
104        if (assignments != null && assignment != null)
105            guessReservation(assignment, true);
106    }
107    
108    /**
109     * Guess the reservation based on the enrollment
110     * @param assignment current assignment
111     * @param onlyAvailable use only reservation that have some space left in them
112     */
113    public void guessReservation(Assignment<Request, Enrollment> assignment, boolean onlyAvailable) {
114        if (iCourse != null) {
115            Reservation best = null;
116            for (Reservation reservation: ((CourseRequest)iRequest).getReservations(iCourse)) {
117                if (reservation.isIncluded(this)) {
118                    if (onlyAvailable && reservation.getContext(assignment).getReservedAvailableSpace(assignment, iRequest) < iRequest.getWeight() && !reservation.canBatchAssignOverLimit())
119                        continue;
120                    if (best == null || best.getPriority() > reservation.getPriority()) {
121                        best = reservation;
122                    } else if (best.getPriority() == reservation.getPriority() &&
123                        best.getContext(assignment).getReservedAvailableSpace(assignment, iRequest) < reservation.getContext(assignment).getReservedAvailableSpace(assignment, iRequest)) {
124                        best = reservation;
125                    }
126                }
127            }
128            iReservation = best;
129        }
130    }
131    
132    /** Student 
133     * @return student
134     **/
135    public Student getStudent() {
136        return iRequest.getStudent();
137    }
138
139    /** Request 
140     * @return request
141     **/
142    public Request getRequest() {
143        return iRequest;
144    }
145
146    /** True if the request is course request 
147     * @return true if the request if course request
148     **/
149    public boolean isCourseRequest() {
150        return iConfig != null;
151    }
152
153    /** Offering of the course request 
154     * @return offering of the course request
155     **/
156    public Offering getOffering() {
157        return (iConfig == null ? null : iConfig.getOffering());
158    }
159
160    /** Config of the course request 
161     * @return config of the course request
162     **/
163    public Config getConfig() {
164        return iConfig;
165    }
166    
167    /** Course of the course request 
168     * @return course of the course request
169     **/
170    public Course getCourse() {
171        return iCourse;
172    }
173
174    /** List of assignments (selected sections) 
175     * @return assignments (selected sections)
176     **/
177    @SuppressWarnings("unchecked")
178    public Set<SctAssignment> getAssignments() {
179        return (Set<SctAssignment>) iAssignments;
180    }
181
182    /** List of sections (only for course request) 
183     * @return selected sections
184     **/
185    @SuppressWarnings("unchecked")
186    public Set<Section> getSections() {
187        if (isCourseRequest())
188            return (Set<Section>) iAssignments;
189        return new HashSet<Section>();
190    }
191
192    /** True when this enrollment is overlapping with the given enrollment 
193     * @param enrl other enrollment
194     * @return true if there is an overlap 
195     **/
196    public boolean isOverlapping(Enrollment enrl) {
197        if (enrl == null || isAllowOverlap() || enrl.isAllowOverlap())
198            return false;
199        for (SctAssignment a : getAssignments()) {
200            if (a.isOverlapping(enrl.getAssignments()))
201                return true;
202        }
203        return false;
204    }
205
206    /** Percent of sections that are wait-listed 
207     * @return percent of sections that are wait-listed
208     **/
209    public double percentWaitlisted() {
210        if (!isCourseRequest())
211            return 0.0;
212        CourseRequest courseRequest = (CourseRequest) getRequest();
213        int nrWaitlisted = 0;
214        for (Section section : getSections()) {
215            if (courseRequest.isWaitlisted(section))
216                nrWaitlisted++;
217        }
218        return ((double) nrWaitlisted) / getAssignments().size();
219    }
220
221    /** Percent of sections that are selected 
222     * @return percent of sections that are selected
223     **/
224    public double percentSelected() {
225        if (!isCourseRequest())
226            return 0.0;
227        CourseRequest courseRequest = (CourseRequest) getRequest();
228        int nrSelected = 0;
229        for (Section section : getSections()) {
230            if (courseRequest.isSelected(section))
231                nrSelected++;
232        }
233        return ((double) nrSelected) / getAssignments().size();
234    }
235    
236    /** Percent of sections that are selected 
237     * @return percent of sections that are selected
238     **/
239    public double percentSelectedSameSection() {
240        if (!isCourseRequest() || getStudent().isDummy()) return (getRequest().hasSelection() ? 1.0 : 0.0);
241        CourseRequest courseRequest = (CourseRequest) getRequest();
242        int nrSelected = 0;
243        Set<Long> nrMatching = new HashSet<Long>();
244        sections: for (Section section : getSections()) {
245            for (Choice choice: courseRequest.getSelectedChoices()) {
246                if (choice.getSubpartId() != null) nrMatching.add(choice.getSubpartId());
247                if (choice.sameSection(section)) {
248                    nrSelected ++; continue sections;
249                }
250            }
251        }
252        return (nrMatching.isEmpty() ? 1.0 : ((double) nrSelected) / nrMatching.size());
253    }
254    
255    /** Percent of sections that have the same configuration 
256     * @return percent of sections that are selected
257     **/
258    public double percentSelectedSameConfig() {
259        if (!isCourseRequest() || getStudent().isDummy() || getConfig() == null) return (getRequest().hasSelection() ? 1.0 : 0.0);
260        CourseRequest courseRequest = (CourseRequest) getRequest();
261        boolean hasConfigSelection = false;
262        for (Choice choice: courseRequest.getSelectedChoices()) {
263            if (choice.getConfigId() != null) {
264                hasConfigSelection = true;
265                if (choice.getConfigId().equals(getConfig().getId())) return 1.0;
266            }
267        }
268        return (hasConfigSelection ? 0.0 : 1.0);
269    }
270
271    /** Percent of sections that are initial 
272     * @return percent of sections that of the initial enrollment
273     **/
274    public double percentInitial() {
275        if (!isCourseRequest())
276            return 0.0;
277        if (getRequest().getInitialAssignment() == null)
278            return 0.0;
279        Enrollment inital = getRequest().getInitialAssignment();
280        int nrInitial = 0;
281        for (Section section : getSections()) {
282            if (inital.getAssignments().contains(section))
283                nrInitial++;
284        }
285        return ((double) nrInitial) / getAssignments().size();
286    }
287    
288    /** Percent of sections that have same time as the initial assignment 
289     * @return percent of sections that have same time as the initial assignment
290     **/
291    public double percentSameTime() {
292        if (!isCourseRequest())
293            return 0.0;
294        Enrollment ie = getRequest().getInitialAssignment();
295        if (ie != null) {
296            int nrInitial = 0;
297            sections: for (Section section : getSections()) {
298                for (Section initial: ie.getSections()) {
299                    if (section.sameInstructionalType(initial) && section.sameTime(initial)) {
300                        nrInitial ++;
301                        continue sections;
302                    }
303                }
304            }
305            return ((double) nrInitial) / getAssignments().size();
306        }
307        Set<Choice> selected = ((CourseRequest)getRequest()).getSelectedChoices();
308        if (!selected.isEmpty()) {
309            int nrInitial = 0;
310            sections: for (Section section : getSections()) {
311                for (Choice choice: selected) {
312                    if (choice.sameInstructionalType(section) && choice.sameTime(section)) {
313                        nrInitial ++;
314                        continue sections;
315                    }
316                    
317                }
318            }
319            return ((double) nrInitial) / getAssignments().size();
320        }
321        return 0.0;
322    }
323
324    /** True if all the sections are wait-listed 
325     * @return all the sections are wait-listed 
326     **/
327    public boolean isWaitlisted() {
328        if (!isCourseRequest())
329            return false;
330        CourseRequest courseRequest = (CourseRequest) getRequest();
331        for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
332            Section section = (Section) i.next();
333            if (!courseRequest.isWaitlisted(section))
334                return false;
335        }
336        return true;
337    }
338
339    /** True if all the sections are selected 
340     * @return all the sections are selected
341     **/
342    public boolean isSelected() {
343        if (!isCourseRequest())
344            return false;
345        CourseRequest courseRequest = (CourseRequest) getRequest();
346        for (Section section : getSections()) {
347            if (!courseRequest.isSelected(section))
348                return false;
349        }
350        return true;
351    }
352
353    /**
354     * Enrollment penalty -- sum of section penalties (see
355     * {@link Section#getPenalty()})
356     * @return online penalty
357     */
358    public double getPenalty() {
359        if (iCachedPenalty == null) {
360            double penalty = 0.0;
361            if (isCourseRequest()) {
362                for (Section section : getSections()) {
363                    penalty += section.getPenalty();
364                }
365            }
366            iCachedPenalty = new Double(penalty / getAssignments().size());
367        }
368        return iCachedPenalty.doubleValue();
369    }
370
371    /** Enrollment value */
372    @Override
373    public double toDouble(Assignment<Request, Enrollment> assignment) {
374        return toDouble(assignment, true);
375    }
376    
377    /** Enrollment value
378     * @param assignment current assignment
379     * @param precise if false, distance conflicts and time overlaps are ignored (i.e., much faster, but less precise computation)
380     * @return enrollment penalty
381     **/
382    public double toDouble(Assignment<Request, Enrollment> assignment, boolean precise) {
383        if (precise)
384            return - getRequest().getWeight() * ((StudentSectioningModel)variable().getModel()).getStudentWeights().getWeight(assignment, this, distanceConflicts(assignment), timeOverlappingConflicts(assignment));
385        else {
386            Double value = (assignment == null ? null : variable().getContext(assignment).getLastWeight());
387            if (value != null) return - value;
388            return - getRequest().getWeight() * ((StudentSectioningModel)variable().getModel()).getStudentWeights().getWeight(assignment, this);
389        }
390    }
391    
392    /** Enrollment name */
393    @Override
394    public String getName() {
395        if (getRequest() instanceof CourseRequest) {
396            Course course = null;
397            CourseRequest courseRequest = (CourseRequest) getRequest();
398            for (Course c : courseRequest.getCourses()) {
399                if (c.getOffering().getConfigs().contains(getConfig())) {
400                    course = c;
401                    break;
402                }
403            }
404            String ret = (course == null ? getConfig() == null ? "" : getConfig().getName() : course.getName());
405            for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
406                Section assignment = (Section) i.next();
407                ret += "\n  " + assignment.getLongName(true) + (i.hasNext() ? "," : "");
408            }
409            return ret;
410        } else if (getRequest() instanceof FreeTimeRequest) {
411            return "Free Time " + ((FreeTimeRequest) getRequest()).getTime().getLongName(true);
412        } else {
413            String ret = "";
414            for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
415                SctAssignment assignment = i.next();
416                ret += assignment.toString() + (i.hasNext() ? "," : "");
417                if (i.hasNext())
418                    ret += "\n  ";
419            }
420            return ret;
421        }
422    }
423
424    public String toString(Assignment<Request, Enrollment> a) {
425        if (getAssignments().isEmpty()) return "not assigned";
426        Set<DistanceConflict.Conflict> dc = distanceConflicts(a);
427        Set<TimeOverlapsCounter.Conflict> toc = timeOverlappingConflicts(a);
428        int share = 0;
429        if (toc != null)
430            for (TimeOverlapsCounter.Conflict c: toc)
431                share += c.getShare();
432        String ret = toDouble(a) + "/" + sDF.format(getRequest().getBound())
433                + (getPenalty() == 0.0 ? "" : "/" + sDF.format(getPenalty()))
434                + (dc == null || dc.isEmpty() ? "" : "/dc:" + dc.size())
435                + (share <= 0 ? "" : "/toc:" + share);
436        if (getRequest() instanceof CourseRequest) {
437            double sameGroup = 0.0; int groupCount = 0;
438            for (RequestGroup g: ((CourseRequest)getRequest()).getRequestGroups()) {
439                if (g.getCourse().equals(getCourse())) {
440                    sameGroup += g.getEnrollmentSpread(a, this, 1.0, 0.0);
441                    groupCount ++;
442                }
443            }
444            if (groupCount > 0)
445                ret += "/g:" + sDF.format(sameGroup / groupCount);
446        }
447        if (getRequest() instanceof CourseRequest) {
448            ret += " ";
449            for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
450                SctAssignment assignment = i.next();
451                ret += assignment + (i.hasNext() ? ", " : "");
452            }
453        }
454        if (getReservation() != null) ret = "(r) " + ret;
455        return ret;
456    }
457    
458    @Override
459    public String toString() {
460        if (getAssignments().isEmpty()) return "not assigned";
461        String ret = sDF.format(getRequest().getBound()) + (getPenalty() == 0.0 ? "" : "/" + sDF.format(getPenalty()));
462        if (getRequest() instanceof CourseRequest) {
463            ret += " ";
464            for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) {
465                SctAssignment assignment = i.next();
466                ret += assignment + (i.hasNext() ? ", " : "");
467            }
468        }
469        if (getReservation() != null) ret = "(r) " + ret;
470        if (getRequest().isMPP()) ret += " [i" + sDF.format(100.0 * percentInitial()) + "/t" + sDF.format(100.0 * percentSameTime()) + "]";
471        return ret;
472    }
473
474    @Override
475    public boolean equals(Object o) {
476        if (o == null || !(o instanceof Enrollment))
477            return false;
478        Enrollment e = (Enrollment) o;
479        if (!ToolBox.equals(getConfig(), e.getConfig()))
480            return false;
481        if (!ToolBox.equals(getRequest(), e.getRequest()))
482            return false;
483        if (!ToolBox.equals(getAssignments(), e.getAssignments()))
484            return false;
485        return true;
486    }
487
488    /** Distance conflicts, in which this enrollment is involved. 
489     * @param assignment current assignment
490     * @return distance conflicts
491     **/
492    public Set<DistanceConflict.Conflict> distanceConflicts(Assignment<Request, Enrollment> assignment) {
493        if (!isCourseRequest())
494            return null;
495        if (getRequest().getModel() instanceof StudentSectioningModel) {
496            DistanceConflict dc = ((StudentSectioningModel) getRequest().getModel()).getDistanceConflict();
497            if (dc == null) return null;
498            return dc.allConflicts(assignment, this);
499        } else
500            return null;
501    }
502
503    /** Time overlapping conflicts, in which this enrollment is involved. 
504     * @param assignment current assignment
505     * @return time overlapping conflicts
506     **/
507    public Set<TimeOverlapsCounter.Conflict> timeOverlappingConflicts(Assignment<Request, Enrollment> assignment) {
508        if (getRequest().getModel() instanceof StudentSectioningModel) {
509            TimeOverlapsCounter toc = ((StudentSectioningModel) getRequest().getModel()).getTimeOverlaps();
510            if (toc == null)
511                return null;
512            return toc.allConflicts(assignment, this);
513        } else
514            return null;
515    }
516
517    /** 
518     * Return enrollment priority
519     * @return zero for the course, one for the first alternative, two for the second alternative
520     */
521    public int getPriority() {
522        return iPriority;
523    }
524    
525    /**
526     * Return total number of slots of all sections in the enrollment.
527     * @return number of slots used
528     */
529    public int getNrSlots() {
530        int ret = 0;
531        for (SctAssignment a: getAssignments()) {
532            if (a.getTime() != null) ret += a.getTime().getLength() * a.getTime().getNrMeetings();
533        }
534        return ret;
535    }
536    
537    /**
538     * Return reservation used for this enrollment
539     * @return used reservation
540     */
541    public Reservation getReservation() { return iReservation; }
542    
543    /**
544     * Set reservation for this enrollment
545     * @param reservation used reservation
546     */
547    public void setReservation(Reservation reservation) { iReservation = reservation; }
548    
549    /**
550     * Time stamp of the enrollment
551     * @return enrollment time stamp
552     */
553    public Long getTimeStamp() {
554        return iTimeStamp;
555    }
556
557    /**
558     * Time stamp of the enrollment
559     * @param timeStamp enrollment time stamp
560     */
561    public void setTimeStamp(Long timeStamp) {
562        iTimeStamp = timeStamp;
563    }
564
565    /**
566     * Approval of the enrollment (only used by the online student sectioning)
567     * @return consent approval
568     */
569    public String getApproval() {
570        return iApproval;
571    }
572
573    /**
574     * Approval of the enrollment (only used by the online student sectioning)
575     * @param approval consent approval
576     */
577    public void setApproval(String approval) {
578        iApproval = approval;
579    }
580    
581    /**
582     * True if this enrollment can overlap with other enrollments of the student.
583     * @return can overlap with other enrollments of the student
584     */
585    public boolean isAllowOverlap() {
586        return (getReservation() != null && getReservation().isAllowOverlap());
587    }
588    
589    /**
590     * 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)  
591     * @return enrollment limit
592     */
593    public int getLimit() {
594        if (!isCourseRequest()) return -1; // free time requests have no limit
595        Integer limit = null;
596        for (Section section: getSections())
597            if (section.getLimit() >= 0) {
598                if (limit == null)
599                    limit = section.getLimit();
600                else
601                    limit = Math.min(limit, section.getLimit());
602            }
603        return (limit == null ? -1 : limit);
604    }
605    
606}