001 package net.sf.cpsolver.studentsct.report; 002 003 import java.text.DecimalFormat; 004 import java.util.Collections; 005 import java.util.Comparator; 006 import java.util.HashSet; 007 import java.util.HashMap; 008 import java.util.Iterator; 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.DistanceMetric; 015 import net.sf.cpsolver.studentsct.StudentSectioningModel; 016 import net.sf.cpsolver.studentsct.extension.DistanceConflict; 017 import net.sf.cpsolver.studentsct.extension.DistanceConflict.Conflict; 018 import net.sf.cpsolver.studentsct.model.Course; 019 import net.sf.cpsolver.studentsct.model.Enrollment; 020 import net.sf.cpsolver.studentsct.model.Request; 021 import net.sf.cpsolver.studentsct.model.Section; 022 import net.sf.cpsolver.studentsct.model.Student; 023 024 /** 025 * This class lists distance student conflicts in a {@link CSVFile} comma 026 * separated text file. Two sections that are attended by the same student are 027 * considered in a distance conflict if they are back-to-back taught in 028 * locations that are two far away. See {@link DistanceConflict} for more 029 * details. <br> 030 * <br> 031 * 032 * Each line represent a pair if courses that have one or more distance 033 * conflicts in between (columns Course1, Course2), column NrStud displays the 034 * number of student distance conflicts (weighted by requests weights), and 035 * column AvgDist displays the average distance for all the distance conflicts 036 * between these two courses. The column NoAlt is Y when every possible 037 * enrollment of the first course is either overlapping or there is a distance 038 * conflict with every possible enrollment of the second course (it is N 039 * otherwise) and a column Reason which lists the sections that are involved in 040 * a distance conflict. 041 * 042 * <br> 043 * <br> 044 * 045 * Usage: new DistanceConflictTable(model),createTable(true, true).save(aFile); 046 * 047 * <br> 048 * <br> 049 * 050 * @version StudentSct 1.2 (Student Sectioning)<br> 051 * Copyright (C) 2007 - 2010 Tomas Muller<br> 052 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 053 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 054 * <br> 055 * This library is free software; you can redistribute it and/or modify 056 * it under the terms of the GNU Lesser General Public License as 057 * published by the Free Software Foundation; either version 3 of the 058 * License, or (at your option) any later version. <br> 059 * <br> 060 * This library is distributed in the hope that it will be useful, but 061 * WITHOUT ANY WARRANTY; without even the implied warranty of 062 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 063 * Lesser General Public License for more details. <br> 064 * <br> 065 * You should have received a copy of the GNU Lesser General Public 066 * License along with this library; if not see 067 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 068 */ 069 public class DistanceConflictTable { 070 private static org.apache.log4j.Logger sLog = org.apache.log4j.Logger.getLogger(DistanceConflictTable.class); 071 private static DecimalFormat sDF = new DecimalFormat("0.000"); 072 073 private StudentSectioningModel iModel = null; 074 private DistanceConflict iDC = null; 075 private DistanceMetric iDM = null; 076 077 /** 078 * Constructor 079 * 080 * @param model 081 * student sectioning model 082 */ 083 public DistanceConflictTable(StudentSectioningModel model) { 084 iModel = model; 085 iDC = model.getDistanceConflict(); 086 if (iDC == null) { 087 iDM = new DistanceMetric(model.getProperties()); 088 iDC = new DistanceConflict(iDM, model.getProperties()); 089 } else { 090 iDM = iDC.getDistanceMetric(); 091 } 092 } 093 094 /** Return student sectioning model */ 095 public StudentSectioningModel getModel() { 096 return iModel; 097 } 098 099 /** 100 * True, if there is no pair of enrollments of r1 and r2 that is not in a 101 * hard conflict and without a distance conflict 102 */ 103 private boolean areInHardConfict(Request r1, Request r2) { 104 for (Enrollment e1 : r1.values()) { 105 for (Enrollment e2 : r2.values()) { 106 if (!e1.isOverlapping(e2) && iDC.nrConflicts(e1, e2) == 0) 107 return false; 108 } 109 } 110 return true; 111 } 112 113 /** 114 * Create report 115 * 116 * @param includeLastLikeStudents 117 * true, if last-like students should be included (i.e., 118 * {@link Student#isDummy()} is true) 119 * @param includeRealStudents 120 * true, if real students should be included (i.e., 121 * {@link Student#isDummy()} is false) 122 * @return report as comma separated text file 123 */ 124 @SuppressWarnings("unchecked") 125 public CSVFile createTable(boolean includeLastLikeStudents, boolean includeRealStudents) { 126 CSVFile csv = new CSVFile(); 127 csv.setHeader(new CSVFile.CSVField[] { new CSVFile.CSVField("Course1"), new CSVFile.CSVField("Course2"), 128 new CSVFile.CSVField("NrStud"), new CSVFile.CSVField("AvgDist"), 129 new CSVFile.CSVField("NoAlt"), new CSVFile.CSVField("Reason") }); 130 Set<Conflict> confs = iDC.computeAllConflicts(); 131 HashMap<Course, HashMap<Course, Object[]>> distConfTable = new HashMap<Course, HashMap<Course, Object[]>>(); 132 for (Conflict conflict : confs) { 133 if (conflict.getStudent().isDummy() && !includeLastLikeStudents) 134 continue; 135 if (!conflict.getStudent().isDummy() && !includeRealStudents) 136 continue; 137 Section s1 = conflict.getS1(), s2 = conflict.getS2(); 138 Course c1 = null, c2 = null; 139 Request r1 = null, r2 = null; 140 for (Request request : conflict.getStudent().getRequests()) { 141 Enrollment enrollment = request.getAssignment(); 142 if (enrollment == null || !enrollment.isCourseRequest()) 143 continue; 144 if (c1 == null && enrollment.getAssignments().contains(s1)) { 145 c1 = enrollment.getCourse(); 146 r1 = request; 147 } 148 if (c2 == null && enrollment.getAssignments().contains(s2)) { 149 c2 = enrollment.getCourse(); 150 r2 = request; 151 } 152 } 153 if (c1 == null) { 154 sLog.error("Unable to find a course for " + s1); 155 continue; 156 } 157 if (c2 == null) { 158 sLog.error("Unable to find a course for " + s2); 159 continue; 160 } 161 if (c1.getName().compareTo(c2.getName()) > 0) { 162 Course x = c1; 163 c1 = c2; 164 c2 = x; 165 Section y = s1; 166 s1 = s2; 167 s2 = y; 168 } 169 if (c1.equals(c2) && s1.getName().compareTo(s2.getName()) > 0) { 170 Section y = s1; 171 s1 = s2; 172 s2 = y; 173 } 174 HashMap<Course, Object[]> firstCourseTable = distConfTable.get(c1); 175 if (firstCourseTable == null) { 176 firstCourseTable = new HashMap<Course, Object[]>(); 177 distConfTable.put(c1, firstCourseTable); 178 } 179 Object[] secondCourseTable = firstCourseTable.get(c2); 180 double nrStud = (secondCourseTable == null ? 0.0 : ((Double) secondCourseTable[0]).doubleValue()) + 1.0; 181 double dist = (secondCourseTable == null ? 0.0 : ((Double) secondCourseTable[1]).doubleValue()) + (conflict.getDistance(iDM)); 182 boolean hard = (secondCourseTable == null ? areInHardConfict(r1, r2) : ((Boolean) secondCourseTable[2]).booleanValue()); 183 HashSet<String> expl = (HashSet<String>) (secondCourseTable == null ? null : secondCourseTable[3]); 184 if (expl == null) 185 expl = new HashSet<String>(); 186 expl.add(s1.getSubpart().getName() + " " + s1.getTime().getLongName() + " " 187 + s1.getPlacement().getRoomName(",") + " vs " + s2.getSubpart().getName() + " " 188 + s2.getTime().getLongName() + " " + s2.getPlacement().getRoomName(",")); 189 firstCourseTable.put(c2, new Object[] { new Double(nrStud), new Double(dist), new Boolean(hard), expl }); 190 } 191 for (Map.Entry<Course, HashMap<Course, Object[]>> entry : distConfTable.entrySet()) { 192 Course c1 = entry.getKey(); 193 HashMap<Course, Object[]> firstCourseTable = entry.getValue(); 194 for (Map.Entry<Course, Object[]> entry2 : firstCourseTable.entrySet()) { 195 Course c2 = entry2.getKey(); 196 Object[] secondCourseTable = entry2.getValue(); 197 HashSet<String> expl = (HashSet<String>) secondCourseTable[3]; 198 String explStr = ""; 199 for (Iterator<String> k = new TreeSet<String>(expl).iterator(); k.hasNext();) 200 explStr += k.next() + (k.hasNext() ? "\n" : ""); 201 double nrStud = ((Double) secondCourseTable[0]).doubleValue(); 202 double dist = ((Double) secondCourseTable[1]).doubleValue() / nrStud; 203 csv.addLine(new CSVFile.CSVField[] { new CSVFile.CSVField(c1.getName()), 204 new CSVFile.CSVField(c2.getName()), new CSVFile.CSVField(sDF.format(nrStud)), 205 new CSVFile.CSVField(sDF.format(dist)), 206 new CSVFile.CSVField(((Boolean) secondCourseTable[2]).booleanValue() ? "Y" : "N"), 207 new CSVFile.CSVField(explStr) }); 208 } 209 } 210 if (csv.getLines() != null) 211 Collections.sort(csv.getLines(), new Comparator<CSVFile.CSVLine>() { 212 @Override 213 public int compare(CSVFile.CSVLine l1, CSVFile.CSVLine l2) { 214 // int cmp = 215 // l2.getField(4).toString().compareTo(l1.getField(4).toString()); 216 // if (cmp!=0) return cmp; 217 int cmp = Double.compare(l2.getField(2).toDouble(), l1.getField(2).toDouble()); 218 if (cmp != 0) 219 return cmp; 220 cmp = l1.getField(0).toString().compareTo(l2.getField(0).toString()); 221 if (cmp != 0) 222 return cmp; 223 return l1.getField(1).toString().compareTo(l2.getField(1).toString()); 224 } 225 }); 226 return csv; 227 } 228 }