001package edu.pdx.cs.joy.grader; 002 003import com.google.common.annotations.VisibleForTesting; 004import edu.pdx.cs.joy.ParserException; 005import edu.pdx.cs.joy.grader.gradebook.*; 006import edu.pdx.cs.joy.grader.gradebook.Assignment.ProjectType; 007 008import java.io.*; 009import java.util.*; 010import java.util.function.Function; 011import java.util.stream.Collectors; 012 013import static edu.pdx.cs.joy.grader.gradebook.Assignment.ProjectType.*; 014 015public class ProjectTimeEstimatesSummary { 016 public TimeEstimatesSummaries getTimeEstimateSummaries(GradeBook book) { 017 return getTimeEstimateSummaries(Collections.singletonList(book)); 018 } 019 020 public TimeEstimatesSummaries getTimeEstimateSummaries(List<GradeBook> books) { 021 TimeEstimatesSummaries summaries = new TimeEstimatesSummaries(); 022 023 Set<ProjectType> projectTypes = new HashSet<>(); 024 for (GradeBook book : books) { 025 book.assignmentsStream() 026 .map(Assignment::getProjectType) 027 .filter(Objects::nonNull) 028 .forEach(projectTypes::add); 029 } 030 031 projectTypes.forEach(projectType -> { 032 summaries.addSummariesFor(books, projectType); 033 }); 034 035 return summaries; 036 } 037 038 public static void main(String[] args) { 039 List<String> gradeBookFileNames = new ArrayList<>(); 040 041 Collections.addAll(gradeBookFileNames, args); 042 043 if (gradeBookFileNames.isEmpty()) { 044 usage("Missing grade book file name"); 045 return; 046 } 047 048 List<GradeBook> gradeBooks = gradeBookFileNames.stream() 049 .map(ProjectTimeEstimatesSummary::getExistingFile) 050 .map(ProjectTimeEstimatesSummary::parseGradeBookFile) 051 .collect(Collectors.toList()); 052 053 ProjectTimeEstimatesSummary summary = new ProjectTimeEstimatesSummary(); 054 TimeEstimatesSummaries summaries = summary.getTimeEstimateSummaries(gradeBooks); 055 056 PrintWriter pw = new PrintWriter(System.out, true); 057 summaries.generateMarkdown(pw, List.of(APP_CLASSES, TEXT_FILE, PRETTY_PRINT, KOANS, XML, DATABASE, REST, ANDROID)); 058 pw.flush(); 059 060 } 061 062 private static GradeBook parseGradeBookFile(File file) { 063 try { 064 return new XmlGradeBookParser(file).parse(); 065 066 } catch (ParserException | IOException e) { 067 usage("While parsing " + file + ": " + e); 068 return null; 069 } 070 } 071 072 private static File getExistingFile(String fileName) { 073 File gradeBookFile = new File(fileName); 074 if (!gradeBookFile.exists()) { 075 usage("Cannot find grade book file: " + gradeBookFile); 076 } 077 return gradeBookFile; 078 } 079 080 private static void usage(String message) { 081 PrintStream err = System.err; 082 err.println("** " + message); 083 err.println(); 084 err.println("usage: ProjectTimeEstimatesSummary gradeBookFile*"); 085 err.println(); 086 087 System.exit(1); 088 } 089 090 static class TimeEstimatesSummaries { 091 private final Map<ProjectType, TimeEstimatesSummary> summaries = new HashMap<>(); 092 093 public TimeEstimatesSummary getTimeEstimateSummary(ProjectType projectType) { 094 return this.summaries.get(projectType); 095 } 096 097 void addSummariesFor(List<GradeBook> books, ProjectType projectType) { 098 Collection<Double> allEstimates = new ArrayList<>(); 099 books.forEach(book -> { 100 book.assignmentsStream() 101 .filter(assignment -> assignment.getProjectType() == projectType) 102 .map(assignment -> getEstimates(book, assignment)) 103 .forEach(allEstimates::addAll); 104 }); 105 106 if (!allEstimates.isEmpty()) { 107 summaries.put(projectType, new TimeEstimatesSummary(allEstimates)); 108 } 109 } 110 111 private Collection<Double> getEstimates(GradeBook book, Assignment assignment) { 112 List<Double> estimates = new ArrayList<>(); 113 114 book.studentsStream().forEach(student -> getEstimate(student, assignment).ifPresent(estimates::add)); 115 116 return estimates; 117 } 118 119 private Optional<Double> getEstimate(Student student, Assignment assignment) { 120 Grade grade = student.getGrade(assignment); 121 if (grade != null) { 122 return getMaximumEstimate(grade.getSubmissionInfos()); 123 } 124 125 return Optional.empty(); 126 } 127 128 private Optional<Double> getMaximumEstimate(List<Grade.SubmissionInfo> submissions) { 129 return submissions.stream() 130 .map(Grade.SubmissionInfo::getEstimatedHours) 131 .filter(Objects::nonNull) 132 .max(Comparator.naturalOrder()); 133 } 134 135 public void generateMarkdown(Writer writer, List<ProjectType> projectTypes) { 136 PrintWriter pw = new PrintWriter(writer, true); 137 headerRows(pw, projectTypes); 138 countRow(pw, projectTypes); 139 averageRow(pw, projectTypes); 140 maximumRow(pw, projectTypes); 141 upperQuartileRow(pw, projectTypes); 142 medianRow(pw, projectTypes); 143 lowerQuartileRow(pw, projectTypes); 144 minimumRow(pw, projectTypes); 145 } 146 147 private void minimumRow(PrintWriter pw, List<ProjectType> projectTypes) { 148 formatRowOfDoubles(pw, projectTypes, "Minimum", TimeEstimatesSummary::getMinimum); 149 } 150 151 private void lowerQuartileRow(PrintWriter pw, List<ProjectType> projectTypes) { 152 formatRowOfDoubles(pw, projectTypes, "Bottom 25%", TimeEstimatesSummary::getLowerQuartile); 153 } 154 155 private void medianRow(PrintWriter pw, List<ProjectType> projectTypes) { 156 formatRowOfDoubles(pw, projectTypes, "Median", TimeEstimatesSummary::getMedian); 157 } 158 159 private void upperQuartileRow(PrintWriter pw, List<ProjectType> projectTypes) { 160 formatRowOfDoubles(pw, projectTypes, "Top 25%", TimeEstimatesSummary::getUpperQuartile); 161 } 162 163 private void maximumRow(PrintWriter pw, List<ProjectType> projectTypes) { 164 formatRowOfDoubles(pw, projectTypes, "Maximum", TimeEstimatesSummary::getMaximum); 165 } 166 167 private void averageRow(PrintWriter pw, List<ProjectType> projectTypes) { 168 formatRowOfDoubles(pw, projectTypes, "Average", TimeEstimatesSummary::getAverage); 169 } 170 171 private void formatRowOfDoubles(PrintWriter pw, List<ProjectType> projectTypes, String rowTitle, Function<TimeEstimatesSummary, Double> cellValue) { 172 formatRow(pw, projectTypes, rowTitle, projectType -> { 173 TimeEstimatesSummary summary = getTimeEstimateSummary(projectType); 174 if (summary == null) { 175 return "n/a"; 176 177 } else { 178 return String.format("%.0f hours", cellValue.apply(summary)); 179 } 180 }); 181 } 182 183 private void formatRow(PrintWriter pw, List<ProjectType> projectTypes, String rowTitle, Function<ProjectType, String> cellValue) { 184 pw.print("| " + rowTitle + " |"); 185 projectTypes.forEach(projectType -> { 186 pw.print(" "); 187 pw.print(cellValue.apply(projectType)); 188 pw.print(" |"); 189 }); 190 pw.println(); 191 } 192 193 private void countRow(PrintWriter pw, List<ProjectType> projectTypes) { 194 formatRow(pw, projectTypes, "Count", projectType -> { 195 TimeEstimatesSummary summary = getTimeEstimateSummary(projectType); 196 if (summary == null) { 197 return "0"; 198 199 } else { 200 return String.valueOf(summary.getCount()); 201 } 202 }); 203 } 204 205 private void headerRows(PrintWriter pw, List<ProjectType> projectTypes) { 206 formatRow(pw, projectTypes, "", TimeEstimatesSummaries::formatProjectType); 207 formatRow(pw, projectTypes, ":---", summary -> "---:"); 208 } 209 210 @VisibleForTesting 211 static String formatProjectType(ProjectType projectType) { 212 switch (projectType) { 213 case APP_CLASSES: 214 return "App Classes"; 215 216 case TEXT_FILE: 217 return "Text File"; 218 219 case PRETTY_PRINT: 220 return "Pretty Print"; 221 222 case XML: 223 return "XML"; 224 225 case DATABASE: 226 return "DATABASE"; 227 228 case KOANS: 229 return "Koans"; 230 231 case REST: 232 return "REST"; 233 234 case ANDROID: 235 return "Android"; 236 237 default: 238 throw new UnsupportedOperationException("Don't know how to format " + projectType); 239 } 240 } 241 } 242 243 static class TimeEstimatesSummary { 244 private final int count; 245 private final double maximum; 246 private final double minimum; 247 private final double median; 248 private final double upperQuartile; 249 private final double lowerQuartile; 250 private final double average; 251 252 TimeEstimatesSummary(Collection<Double> estimates) { 253 this.count = estimates.size(); 254 this.average = estimates.stream().reduce(Double::sum).get() / ((double) this.count); 255 List<Double> sorted = estimates.stream().sorted().collect(Collectors.toList()); 256 this.minimum = sorted.get(0); 257 this.maximum = sorted.get(sorted.size() - 1); 258 this.median = median(sorted); 259 this.upperQuartile = upperQuartile(sorted); 260 this.lowerQuartile = lowerQuartile(sorted); 261 } 262 263 @VisibleForTesting 264 static double lowerQuartile(List<Double> doubles) { 265 int midway = (int) Math.ceil(doubles.size() / 2.0f); 266 return median(doubles.subList(0, midway)); 267 } 268 269 @VisibleForTesting 270 static double upperQuartile(List<Double> doubles) { 271 int midway = (int) Math.floor(doubles.size() / 2.0f); 272 return median(doubles.subList(midway, doubles.size())); 273 } 274 275 @VisibleForTesting 276 static double median(Collection<Double> doubles) { 277 List<Double> values = doubles.stream().sorted().collect(Collectors.toList()); 278 return median(values); 279 } 280 281 private static double median(List<Double> doubles) { 282 int size = doubles.size(); 283 int midpoint = (size / 2); 284 if (size % 2 == 0) { 285 return average(doubles.get(midpoint - 1), doubles.get(midpoint)); 286 287 } else { 288 return doubles.get(midpoint); 289 } 290 } 291 292 private static double average(double d1, double d2) { 293 return (d1 + d2) / 2.0; 294 } 295 296 public int getCount() { 297 return count; 298 } 299 300 public double getMaximum() { 301 return maximum; 302 } 303 304 public double getMinimum() { 305 return minimum; 306 } 307 308 public double getMedian() { 309 return median; 310 } 311 312 public double getUpperQuartile() { 313 return upperQuartile; 314 } 315 316 public double getLowerQuartile() { 317 return lowerQuartile; 318 } 319 320 public double getAverage() { 321 return average; 322 } 323 324 } 325}