001package edu.pdx.cs410J.grader; 002 003import com.google.common.annotations.VisibleForTesting; 004import edu.pdx.cs410J.ParserException; 005import edu.pdx.cs410J.grader.gradebook.*; 006import edu.pdx.cs410J.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.cs410J.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, 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 KOANS: 226 return "Koans"; 227 228 case REST: 229 return "REST"; 230 231 case ANDROID: 232 return "Android"; 233 234 default: 235 throw new UnsupportedOperationException("Don't know how to format " + projectType); 236 } 237 } 238 } 239 240 static class TimeEstimatesSummary { 241 private final int count; 242 private final double maximum; 243 private final double minimum; 244 private final double median; 245 private final double upperQuartile; 246 private final double lowerQuartile; 247 private final double average; 248 249 TimeEstimatesSummary(Collection<Double> estimates) { 250 this.count = estimates.size(); 251 this.average = estimates.stream().reduce(Double::sum).get() / ((double) this.count); 252 List<Double> sorted = estimates.stream().sorted().collect(Collectors.toList()); 253 this.minimum = sorted.get(0); 254 this.maximum = sorted.get(sorted.size() - 1); 255 this.median = median(sorted); 256 this.upperQuartile = upperQuartile(sorted); 257 this.lowerQuartile = lowerQuartile(sorted); 258 } 259 260 @VisibleForTesting 261 static double lowerQuartile(List<Double> doubles) { 262 int midway = (int) Math.ceil(doubles.size() / 2.0f); 263 return median(doubles.subList(0, midway)); 264 } 265 266 @VisibleForTesting 267 static double upperQuartile(List<Double> doubles) { 268 int midway = (int) Math.floor(doubles.size() / 2.0f); 269 return median(doubles.subList(midway, doubles.size())); 270 } 271 272 @VisibleForTesting 273 static double median(Collection<Double> doubles) { 274 List<Double> values = doubles.stream().sorted().collect(Collectors.toList()); 275 return median(values); 276 } 277 278 private static double median(List<Double> doubles) { 279 int size = doubles.size(); 280 int midpoint = (size / 2); 281 if (size % 2 == 0) { 282 return average(doubles.get(midpoint - 1), doubles.get(midpoint)); 283 284 } else { 285 return doubles.get(midpoint); 286 } 287 } 288 289 private static double average(double d1, double d2) { 290 return (d1 + d2) / 2.0; 291 } 292 293 public int getCount() { 294 return count; 295 } 296 297 public double getMaximum() { 298 return maximum; 299 } 300 301 public double getMinimum() { 302 return minimum; 303 } 304 305 public double getMedian() { 306 return median; 307 } 308 309 public double getUpperQuartile() { 310 return upperQuartile; 311 } 312 313 public double getLowerQuartile() { 314 return lowerQuartile; 315 } 316 317 public double getAverage() { 318 return average; 319 } 320 321 } 322}