001package edu.pdx.cs410J.grader;
002
003import com.google.common.annotations.VisibleForTesting;
004import edu.pdx.cs410J.ParserException;
005import edu.pdx.cs410J.grader.gradebook.*;
006
007import java.io.*;
008import java.text.NumberFormat;
009import java.text.SimpleDateFormat;
010import java.time.LocalDateTime;
011import java.time.format.DateTimeFormatter;
012import java.time.format.FormatStyle;
013import java.util.*;
014import java.util.function.Supplier;
015import java.util.stream.Stream;
016
017import static edu.pdx.cs410J.grader.gradebook.Student.Section.*;
018
019/**
020 * Class that creates a pretty report that summarizes a student's
021 * grades.
022 */
023public class SummaryReport {
024  static final String UNDERGRADUATE_DIRECTORY_NAME = "undergraduates";
025  static final String GRADUATE_DIRECTORY_NAME = "graduates";
026  private static final HashMap<Student, Double> allTotals = new HashMap<>();
027
028  /**
029   * Computes the student's final average and makes a pretty report.
030   */
031  @VisibleForTesting
032  static void dumpReportTo(GradeBook book, Student student,
033                           PrintWriter pw, boolean assignLetterGrades) {
034    NumberFormat format = NumberFormat.getNumberInstance();
035    format.setMinimumFractionDigits(1);
036    format.setMaximumFractionDigits(1);
037
038    Assignment lowestQuiz = null;
039    double best = 0.0;
040    double total = 0.0;
041
042    pw.println("Grade summary for: " + student.getFullName());
043    SimpleDateFormat df = 
044      new SimpleDateFormat("EEEE MMMM d, yyyy 'at' h:mm a");
045    pw.println("Generated on: " + df.format(new Date()));
046    pw.println("");
047
048    for (String assignmentName : getSortedAssignmentNames(book)) {
049      Assignment assignment = book.getAssignment(assignmentName);
050      if (noStudentHasGradeFor(assignment, book)) {
051        continue;
052      }
053
054      Grade grade = student.getGrade(assignmentName);
055      double score = getScore(grade);
056
057
058//       System.out.println("Examining " + assign + ", score: " + score);
059
060      if (assignment.getType() == Assignment.AssignmentType.QUIZ) {
061        if (lowestQuiz == null) {
062          lowestQuiz = assignment;
063//           System.out.println("Lowest quiz: " + lowestQuiz + 
064//                              ", score: " + score);
065
066        } else {
067          Grade lowestGrade = student.getGrade(lowestQuiz.getName());
068          if (lowestGrade != null && score < lowestGrade.getScore()) {
069            lowestQuiz = assignment;
070//             System.out.println("Lowest quiz: " + lowestQuiz + ", score: "
071//                                + score + ", lowest grade: " +
072//                                student.getGrade(lowestQuiz.getName()));
073          }
074        }
075      }
076
077      StringBuffer line = new StringBuffer();
078      line.append("  ");
079      line.append(assignment.getName());
080      line.append(" (");
081      line.append(assignment.getDescription());
082      line.append(")");
083      if (assignment.getType() == Assignment.AssignmentType.OPTIONAL) {
084        line.append(" (OPTIONAL)");
085      }
086      line.append(": ");
087      line.append(format.format(score));
088      line.append("/");
089      line.append(format.format(assignment.getPoints()));
090
091
092      // Skip incompletes and no grades
093      if (grade == null) {
094        if (dueDateHasPassed(assignment)) {
095          line.append(" (MISSING GRADE)");
096
097        } else {
098          line.append(String.format(" (due %s)", formatDueDate(assignment)));
099        }
100
101      } else if (grade.isIncomplete()) {
102        line.append(" (INCOMPLETE)");
103
104      } else if (grade.isNotGraded()) {
105        line.append( " (NOT GRADED)");
106      }
107
108      pw.println(line);
109
110      // Don't count optional assignments toward the maximum point
111      // total
112      if (assignment.getType() != Assignment.AssignmentType.OPTIONAL) {
113        best += assignment.getPoints();
114      }
115
116      total += score;
117    }
118
119    if (lowestQuiz != null) {
120      pw.println("");
121      pw.println("Lowest Quiz grade dropped: " +
122                  lowestQuiz.getName());
123      pw.println("");
124
125      // Subtract lowest quiz grade
126      Grade lowestGrade = student.getGrade(lowestQuiz.getName());
127      total -= (lowestGrade != null ? lowestGrade.getScore() : 0);
128      best -= lowestQuiz.getPoints();
129    }
130
131    // Print out late and resubmitted assignments
132    pw.println("Late assignments:");
133    for (String late : student.getLate()) {
134      pw.println("  " + late);
135    }
136    pw.println();
137
138    pw.println("Resubmitted assignments:");
139    for (String resubmitted : student.getResubmitted()) {
140      pw.println("  " + resubmitted);
141    }
142    pw.println("");
143
144    pw.println("Total grade: " + format.format(total)  + "/" +
145               format.format(best));
146
147    double overallScore = total / best;
148
149    if (assignLetterGrades) {
150      LetterGrade letterGrade = book.getLetterGradeForScore(student.getEnrolledSection(), overallScore * 100.0);
151      student.setLetterGrade(letterGrade);
152    }
153
154    if (student.getLetterGrade() != null) {
155      pw.println("Letter Grade: " + student.getLetterGrade());
156    }
157
158    allTotals.put(student, overallScore);
159  }
160
161  private static String formatDueDate(Assignment assignment) {
162    LocalDateTime dueDate = assignment.getDueDate();
163    return dueDate.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT));
164  }
165
166  @VisibleForTesting
167  static boolean dueDateHasPassed(Assignment assignment) {
168    LocalDateTime dueDate = assignment.getDueDate();
169    LocalDateTime now = LocalDateTime.now();
170    return now.isAfter(dueDate);
171  }
172
173  static boolean noStudentHasGradeFor(Assignment assignment, GradeBook book) {
174    boolean noAssignmentIsGraded = book.studentsStream()
175      .map(student -> getGrade(assignment, student))
176      .noneMatch(grade -> grade != null && !grade.isNotGraded());
177
178    boolean allAssignmentHaveGradeOfZero = book.studentsStream()
179      .map(student -> getGrade(assignment, student))
180      .allMatch(grade -> grade != null && grade.getScore() == 0.0);
181
182    return noAssignmentIsGraded || allAssignmentHaveGradeOfZero;
183  }
184
185  private static Grade getGrade(Assignment assignment, Student student) {
186    return student.getGrade(assignment.getName());
187  }
188
189  private static double getScore(Grade grade) {
190    // Average non-existent scores as zero
191    double score;
192    if (grade == null) {
193      score = 0.0;
194
195    } else {
196      score = grade.getScore();
197    }
198    return score;
199  }
200
201  private static SortedSet<String> getSortedAssignmentNames(GradeBook book) {
202    return new TreeSet<>(book.getAssignmentNames());
203  }
204
205  private static final PrintWriter out = new PrintWriter(System.out, true);
206  private static final PrintWriter err = new PrintWriter(System.err, true);
207
208  /**
209   * Prints usage information about this main program
210   */
211  private static void usage(String s) {
212    err.println("\n** " + s + "\n");
213    err.println("\njava SummaryReport -assignLetterGrades xmlFile outDir (student)*");
214    err.println("\n");
215    err.println("Generates summary grade reports for the given " +
216                "students.  If student is not \ngiven, then reports " + 
217                "for all students are generated.");
218    err.println("");
219    System.exit(1);
220  }
221
222  /**
223   * Main program that creates summary reports for every student in a
224   * grade book located in a given XML file.
225   */
226  public static void main(String[] args) {
227    boolean assignLetterGrades = false;
228    String xmlFileName = null;
229    String outputDirName = null;
230    Collection<String> students = new ArrayList<>();
231
232    for (String arg : args) {
233      if (arg.equals("-assignLetterGrades")) {
234        assignLetterGrades = true;
235
236      } else if (xmlFileName == null) {
237        xmlFileName = arg;
238
239      } else if (outputDirName == null) {
240        outputDirName = arg;
241
242      } else {
243        students.add(arg);
244      }
245    }
246
247    if (xmlFileName == null) {
248      usage("Missing XML file name");
249    }
250
251    if (outputDirName == null) {
252      usage("Missing output dir name");
253      return;
254    }
255
256    String xmlFile = xmlFileName;
257    File outDir = new File(outputDirName);
258    if (!outDir.exists()) {
259      outDir.mkdirs();
260
261    } else if (!outDir.isDirectory()) {
262      usage(outDir + " is not a directory");
263    }
264
265    File file = new File(xmlFile);
266    if (!file.exists()) {
267      usage("Grade book file " + xmlFile + " does not exist");
268    }
269
270    GradeBook book = parseGradeBook(file);
271
272    // Create a SummaryReport for every student
273    Iterable<String> studentIds;
274
275    if (!students.isEmpty()) {
276      studentIds = students;
277
278    } else {
279      studentIds = book.getStudentIds();
280    }
281
282    dumpReports(studentIds, book, outDir, assignLetterGrades);
283
284    // Sort students by totals and print out results:
285    Set<Student> students1 = allTotals.keySet();
286    printOutStudentTotals(students1, out);
287
288    saveGradeBookIfDirty(xmlFileName, book);
289
290  }
291
292  @VisibleForTesting
293  static void printOutStudentTotals(Set<Student> allStudents, PrintWriter out) {
294    SortedSet<Student> sorted = getStudentSortedByTotalPoints(allStudents);
295
296    out.println("Undergraduates:");
297    Stream<Student> undergrads = sorted.stream().filter(student -> student.getEnrolledSection() == UNDERGRADUATE);
298    printOutStudentTotals(out, undergrads);
299
300    out.println("Graduate Students:");
301    Stream<Student> grads = sorted.stream().filter(student -> student.getEnrolledSection() == GRADUATE);
302    printOutStudentTotals(out, grads);
303
304  }
305
306  private static void printOutStudentTotals(PrintWriter out, Stream<Student> students) {
307    NumberFormat format = NumberFormat.getPercentInstance();
308
309    students.forEach(student -> {
310      Double d = allTotals.get(student);
311      out.print("  " + student + ": " + format.format(d.doubleValue()));
312
313      if (student.getLetterGrade() != null) {
314        out.print(" " + student.getLetterGrade());
315      }
316
317      out.println();
318    });
319  }
320
321  private static void saveGradeBookIfDirty(String xmlFileName, GradeBook book) {
322    if (book.isDirty()) {
323      try {
324        XmlDumper dumper = new XmlDumper(xmlFileName);
325        dumper.dump(book);
326
327      } catch (IOException ex) {
328        printErrorMessageAndExit("While saving gradebook to " + xmlFileName, ex);
329      }
330    }
331  }
332
333  private static SortedSet<Student> getStudentSortedByTotalPoints(Set<Student> students) {
334    SortedSet<Student> sorted = new TreeSet<>((s1, s2) -> {
335      Double d1 = allTotals.get(s1);
336      Double d2 = allTotals.get(s2);
337      if (d2.compareTo(d1) == 0) {
338        return s1.getId().compareTo(s2.getId());
339      } else {
340        return d2.compareTo(d1);
341      }
342    });
343
344    sorted.addAll(students);
345    return sorted;
346  }
347
348  @VisibleForTesting
349  static void dumpReports(Iterable<String> studentIds, GradeBook book, File outDir, boolean assignLetterGrades) {
350    for (String id : studentIds) {
351      err.println(id);
352
353      Student student = book.getStudent(id).orElseThrow(noStudentWithId(id));
354
355      File outFile = new File(getDirectoryForReportFileForStudent(outDir, student), getReportFileName(id));
356      try {
357        PrintWriter pw =
358          new PrintWriter(new FileWriter(outFile), true);
359        dumpReportTo(book, student, pw, assignLetterGrades);
360
361//         dumpReportTo(book, student, out);
362      } catch (IOException ex) {
363        printErrorMessageAndExit("While writing report to " + outFile, ex);
364      }
365    }
366  }
367
368  private static File getDirectoryForReportFileForStudent(File parentDirectory, Student student) {
369    String directoryName;
370    Student.Section enrolledSection = student.getEnrolledSection();
371    switch (enrolledSection) {
372      case UNDERGRADUATE:
373        directoryName = UNDERGRADUATE_DIRECTORY_NAME;
374        break;
375      case GRADUATE:
376        directoryName = GRADUATE_DIRECTORY_NAME;
377        break;
378      default:
379        throw new IllegalStateException("Don't know directory name for " + enrolledSection);
380    }
381    File directory = new File(parentDirectory, directoryName);
382    directory.mkdirs();
383    return directory;
384  }
385
386  @VisibleForTesting
387  static String getReportFileName(String studentId) {
388    return studentId + ".report";
389  }
390
391  private static GradeBook parseGradeBook(File gradeBookFile) {
392    GradeBook book = null;
393    try {
394      err.println("Parsing " + gradeBookFile);
395      XmlGradeBookParser parser = new XmlGradeBookParser(gradeBookFile);
396      book = parser.parse();
397
398    } catch (FileNotFoundException ex) {
399      printErrorMessageAndExit("** Could not find grade book file: " + gradeBookFile, ex);
400
401    } catch (ParserException | IOException ex) {
402      printErrorMessageAndExit("** Exception while parsing " + gradeBookFile, ex);
403    }
404    return book;
405  }
406
407  private static void printErrorMessageAndExit(String message, Throwable ex) {
408    err.println(message);
409    ex.printStackTrace(err);
410    System.exit(1);
411  }
412
413  @SuppressWarnings("ThrowableInstanceNeverThrown")
414  private static Supplier<? extends IllegalStateException> noStudentWithId(String id) {
415    return () -> new IllegalStateException("No student with id " + id);
416  }
417
418}