    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}