001package org.cpsolver.studentsct.report;
002
003import java.text.DecimalFormat;
004import java.util.ArrayList;
005import java.util.Comparator;
006import java.util.HashMap;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Map;
010import java.util.Set;
011import java.util.TreeSet;
012
013import org.cpsolver.ifs.assignment.Assignment;
014import org.cpsolver.ifs.util.CSVFile;
015import org.cpsolver.ifs.util.DataProperties;
016import org.cpsolver.studentsct.StudentSectioningModel;
017import org.cpsolver.studentsct.extension.TimeOverlapsCounter;
018import org.cpsolver.studentsct.extension.TimeOverlapsCounter.Conflict;
019import org.cpsolver.studentsct.model.Course;
020import org.cpsolver.studentsct.model.Enrollment;
021import org.cpsolver.studentsct.model.FreeTimeRequest;
022import org.cpsolver.studentsct.model.Request;
023import org.cpsolver.studentsct.model.SctAssignment;
024import org.cpsolver.studentsct.model.Section;
025import org.cpsolver.studentsct.model.Student;
026
027
028/**
029 * This class lists time overlapping conflicts in a {@link CSVFile} comma
030 * separated text file. Only sections that allow overlaps
031 * (see {@link SctAssignment#isAllowOverlap()}) can overlap. See {@link TimeOverlapsCounter} for more
032 * details. <br>
033 * <br>
034 * 
035 * Each line represent a pair if classes that overlap in time and have one
036 * or more students in common.
037 * 
038 * <br>
039 * <br>
040 * 
041 * Usage: new DistanceConflictTable(model),createTable(true, true).save(aFile);
042 * 
043 * <br>
044 * <br>
045 * 
046 * @version StudentSct 1.3 (Student Sectioning)<br>
047 *          Copyright (C) 2013 - 2014 Tomáš Müller<br>
048 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
049 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
050 * <br>
051 *          This library is free software; you can redistribute it and/or modify
052 *          it under the terms of the GNU Lesser General Public License as
053 *          published by the Free Software Foundation; either version 3 of the
054 *          License, or (at your option) any later version. <br>
055 * <br>
056 *          This library is distributed in the hope that it will be useful, but
057 *          WITHOUT ANY WARRANTY; without even the implied warranty of
058 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
059 *          Lesser General Public License for more details. <br>
060 * <br>
061 *          You should have received a copy of the GNU Lesser General Public
062 *          License along with this library; if not see
063 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
064 */
065public class TimeOverlapConflictTable implements StudentSectioningReport {
066    private static DecimalFormat sDF1 = new DecimalFormat("0.####");
067    private static DecimalFormat sDF2 = new DecimalFormat("0.0000");
068
069    private StudentSectioningModel iModel = null;
070    private TimeOverlapsCounter iTOC = null;
071
072    /**
073     * Constructor
074     * 
075     * @param model
076     *            student sectioning model
077     */
078    public TimeOverlapConflictTable(StudentSectioningModel model) {
079        iModel = model;
080        iTOC = model.getTimeOverlaps();
081        if (iTOC == null) {
082            iTOC = new TimeOverlapsCounter(null, model.getProperties());
083        }
084    }
085
086    /** Return student sectioning model 
087     * @return problem model
088     **/
089    public StudentSectioningModel getModel() {
090        return iModel;
091    }
092
093    /**
094     * Create report
095     * 
096     * @param assignment current assignment
097     * @param includeLastLikeStudents
098     *            true, if last-like students should be included (i.e.,
099     *            {@link Student#isDummy()} is true)
100     * @param includeRealStudents
101     *            true, if real students should be included (i.e.,
102     *            {@link Student#isDummy()} is false)
103     * @param useAmPm use 12-hour format
104     * @return report as comma separated text file
105     */
106    public CSVFile createTable(Assignment<Request, Enrollment> assignment, boolean includeLastLikeStudents, boolean includeRealStudents, boolean useAmPm) {
107        CSVFile csv = new CSVFile();
108        csv.setHeader(new CSVFile.CSVField[] { new CSVFile.CSVField("Course"), new CSVFile.CSVField("Total\nConflicts"),
109                new CSVFile.CSVField("Class"), new CSVFile.CSVField("Meeting Time"),
110                new CSVFile.CSVField("Time\nConflicts"), new CSVFile.CSVField("% of Total\nConflicts"),
111                new CSVFile.CSVField("Conflicting\nClass"), new CSVFile.CSVField("Conflicting\nMeeting Time"),
112                new CSVFile.CSVField("Overlap [min]"), new CSVFile.CSVField("Joined\nConflicts"), new CSVFile.CSVField("% of Total\nConflicts")
113                });
114        
115        Set<Conflict> confs = new HashSet<Conflict>();
116        for (Request r1 : getModel().variables()) {
117            Enrollment e1 = assignment.getValue(r1);
118            if (e1 == null || r1 instanceof FreeTimeRequest)
119                continue;
120            for (Request r2 : r1.getStudent().getRequests()) {
121                Enrollment e2 = assignment.getValue(r2);
122                if (r2 instanceof FreeTimeRequest) {
123                    FreeTimeRequest ft = (FreeTimeRequest)r2;
124                    confs.addAll(iTOC.conflicts(e1, ft.createEnrollment()));
125                } else if (e2 != null && r1.getId() < r2.getId()) {
126                    confs.addAll(iTOC.conflicts(e1, e2));
127                }
128            }
129        }
130        
131        HashMap<Course, Set<Long>> totals = new HashMap<Course, Set<Long>>();
132        HashMap<CourseSection, Map<CourseSection, Double>> conflictingPairs = new HashMap<CourseSection, Map<CourseSection,Double>>();
133        HashMap<CourseSection, Set<Long>> sectionOverlaps = new HashMap<CourseSection, Set<Long>>();        
134        
135        for (Conflict conflict : confs) {
136            if (conflict.getStudent().isDummy() && !includeLastLikeStudents) continue;
137            if (!conflict.getStudent().isDummy() && !includeRealStudents) continue;
138            if (conflict.getR1() == null || conflict.getR1() instanceof FreeTimeRequest || conflict.getR2() == null || conflict.getR2() instanceof FreeTimeRequest) continue;
139            Section s1 = (Section)conflict.getS1(), s2 = (Section)conflict.getS2();
140            Request r1 = conflict.getR1();
141            Course c1 = assignment.getValue(conflict.getR1()).getCourse();
142            Request r2 = conflict.getR2();
143            Course c2 = assignment.getValue(conflict.getR2()).getCourse();
144            CourseSection a = new CourseSection(c1, s1);
145            CourseSection b = new CourseSection(c2, s2);
146            
147            Set<Long> students = totals.get(c1);
148            if (students == null) {
149                students = new HashSet<Long>();
150                totals.put(c1, students);
151            }
152            students.add(r1.getStudent().getId());
153            students = totals.get(c2);
154            if (students == null) {
155                students = new HashSet<Long>();
156                totals.put(c2, students);
157            }
158            students.add(r2.getStudent().getId());
159            
160            Set<Long> total = sectionOverlaps.get(a);
161            if (total == null) {
162                total = new HashSet<Long>();
163                sectionOverlaps.put(a, total);
164            }
165            total.add(r1.getStudent().getId());
166            Map<CourseSection, Double> pair = conflictingPairs.get(a);
167            if (pair == null) {
168                pair = new HashMap<CourseSection, Double>();
169                conflictingPairs.put(a, pair);
170            }
171            Double prev = pair.get(b);
172            pair.put(b, r2.getWeight() + (prev == null ? 0.0 : prev.doubleValue()));
173            
174            total = sectionOverlaps.get(b);
175            if (total == null) {
176                total = new HashSet<Long>();
177                sectionOverlaps.put(b, total);
178            }
179            total.add(r2.getStudent().getId());
180            pair = conflictingPairs.get(b);
181            if (pair == null) {
182                pair = new HashMap<CourseSection, Double>();
183                conflictingPairs.put(b, pair);
184            }
185            prev = pair.get(a);
186            pair.put(a, r1.getWeight() + (prev == null ? 0.0 : prev.doubleValue()));
187        }
188        
189        Comparator<Course> courseComparator = new Comparator<Course>() {
190            @Override
191            public int compare(Course a, Course b) {
192                int cmp = a.getName().compareTo(b.getName());
193                if (cmp != 0) return cmp;
194                return a.getId() < b.getId() ? -1 : a.getId() == b.getId() ? 0 : 1;
195            }
196        };
197        Comparator<Section> sectionComparator = new Comparator<Section>() {
198            @Override
199            public int compare(Section a, Section b) {
200                int cmp = a.getSubpart().getConfig().getOffering().getName().compareTo(b.getSubpart().getConfig().getOffering().getName());
201                if (cmp != 0) return cmp;
202                cmp = a.getSubpart().getInstructionalType().compareTo(b.getSubpart().getInstructionalType());
203                // if (cmp != 0) return cmp;
204                // cmp = a.getName().compareTo(b.getName());
205                if (cmp != 0) return cmp;
206                return a.getId() < b.getId() ? -1 : a.getId() == b.getId() ? 0 : 1;
207            }
208        };
209        
210        TreeSet<Course> courses = new TreeSet<Course>(courseComparator);
211        courses.addAll(totals.keySet());
212        for (Course course: courses) {
213            Set<Long> total = totals.get(course);
214            
215            TreeSet<Section> sections = new TreeSet<Section>(sectionComparator);
216            for (Map.Entry<CourseSection, Set<Long>> entry: sectionOverlaps.entrySet())
217                if (course.equals(entry.getKey().getCourse()))
218                    sections.add(entry.getKey().getSection());
219            
220            boolean firstCourse = true;
221            for (Section section: sections) {
222                Set<Long> sectionOverlap = sectionOverlaps.get(new CourseSection(course, section));
223                Map<CourseSection, Double> pair = conflictingPairs.get(new CourseSection(course, section));
224                boolean firstClass = true;
225                
226                for (CourseSection other: new TreeSet<CourseSection>(pair.keySet())) {
227                    List<CSVFile.CSVField> line = new ArrayList<CSVFile.CSVField>();
228                    line.add(new CSVFile.CSVField(firstCourse && firstClass ? course.getName() : ""));
229                    line.add(new CSVFile.CSVField(firstCourse && firstClass ? total.size() : ""));
230                    
231                    line.add(new CSVFile.CSVField(firstClass ? section.getSubpart().getName() + " " + section.getName(course.getId()): ""));
232                    line.add(new CSVFile.CSVField(firstClass ? section.getTime() == null ? "" : section.getTime().getDayHeader() + " " + section.getTime().getStartTimeHeader(useAmPm) + " - " + section.getTime().getEndTimeHeader(useAmPm): ""));
233                        
234                    line.add(new CSVFile.CSVField(firstClass && sectionOverlap != null ? String.valueOf(sectionOverlap.size()): ""));
235                    line.add(new CSVFile.CSVField(firstClass && sectionOverlap != null ? sDF2.format(((double)sectionOverlap.size()) / total.size()) : ""));
236
237                    line.add(new CSVFile.CSVField(other.getCourse().getName() + " " + other.getSection().getSubpart().getName() + " " + other.getSection().getName(other.getCourse().getId())));
238                    line.add(new CSVFile.CSVField(other.getSection().getTime().getDayHeader() + " " + other.getSection().getTime().getStartTimeHeader(useAmPm) + " - " + other.getSection().getTime().getEndTimeHeader(useAmPm)));
239                    
240                    line.add(new CSVFile.CSVField(sDF1.format(5 * iTOC.share(section, other.getSection()))));
241                    line.add(new CSVFile.CSVField(sDF1.format(pair.get(other))));
242                    line.add(new CSVFile.CSVField(sDF2.format(pair.get(other) / total.size())));
243                    
244                    csv.addLine(line);
245                    firstClass = false;
246                }                    
247                firstCourse = false;
248            }
249            
250            csv.addLine();
251        }
252        
253        
254        return csv;
255    }
256
257    @Override
258    public CSVFile create(Assignment<Request, Enrollment> assignment, DataProperties properties) {
259        return createTable(assignment, properties.getPropertyBoolean("lastlike", false), properties.getPropertyBoolean("real", true), properties.getPropertyBoolean("useAmPm", true));
260    }
261}