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