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