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}