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}