package edu.pdx.cs410J.grader;
import com.google.common.annotations.VisibleForTesting;
import edu.pdx.cs410J.ParserException;
import java.io.*;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Stream;
import static edu.pdx.cs410J.grader.Student.Section.*;
/**
* Class that creates a pretty report that summarizes a student's
* grades.
*/
public class SummaryReport {
static final String UNDERGRADUATE_DIRECTORY_NAME = "undergraduates";
static final String GRADUATE_DIRECTORY_NAME = "graduates";
private static HashMap<Student, Double> allTotals = new HashMap<>();
/**
* Computes the student's final average and makes a pretty report.
*/
@VisibleForTesting
static void dumpReportTo(GradeBook book, Student student,
PrintWriter pw, boolean assignLetterGrades) {
NumberFormat format = NumberFormat.getNumberInstance();
format.setMinimumFractionDigits(1);
format.setMaximumFractionDigits(1);
Assignment lowestQuiz = null;
double best = 0.0;
double total = 0.0;
pw.println("Grade summary for: " + student.getFullName());
SimpleDateFormat df =
new SimpleDateFormat("EEEE MMMM d, yyyy 'at' h:mm a");
pw.println("Generated on: " + df.format(new Date()));
pw.println("");
for (String assignmentName : getSortedAssignmentNames(book)) {
Assignment assignment = book.getAssignment(assignmentName);
if (noStudentHasGradeFor(assignment, book)) {
continue;
}
Grade grade = student.getGrade(assignmentName);
double score = getScore(grade);
// System.out.println("Examining " + assign + ", score: " + score);
if (assignment.getType() == Assignment.AssignmentType.QUIZ) {
if (lowestQuiz == null) {
lowestQuiz = assignment;
// System.out.println("Lowest quiz: " + lowestQuiz +
// ", score: " + score);
} else {
Grade lowestGrade = student.getGrade(lowestQuiz.getName());
if (lowestGrade != null && score < lowestGrade.getScore()) {
lowestQuiz = assignment;
// System.out.println("Lowest quiz: " + lowestQuiz + ", score: "
// + score + ", lowest grade: " +
// student.getGrade(lowestQuiz.getName()));
}
}
}
StringBuffer line = new StringBuffer();
line.append(" ");
line.append(assignment.getName());
line.append(" (");
line.append(assignment.getDescription());
line.append(")");
if (assignment.getType() == Assignment.AssignmentType.OPTIONAL) {
line.append(" (OPTIONAL)");
}
line.append(": ");
line.append(format.format(score));
line.append("/");
line.append(format.format(assignment.getPoints()));
// Skip incompletes and no grades
if (grade == null) {
line.append(" (MISSING GRADE)");
} else if (grade.isIncomplete()) {
line.append(" (INCOMPLETE)");
} else if (grade.isNotGraded()) {
line.append( "(NOT GRADED)");
}
pw.println(line);
// Don't count optional assignments toward the maximum point
// total
if (assignment.getType() != Assignment.AssignmentType.OPTIONAL) {
best += assignment.getPoints();
}
total += score;
}
if (lowestQuiz != null) {
pw.println("");
pw.println("Lowest Quiz grade dropped: " +
lowestQuiz.getName());
pw.println("");
// Subtract lowest quiz grade
Grade lowestGrade = student.getGrade(lowestQuiz.getName());
total -= (lowestGrade != null ? lowestGrade.getScore() : 0);
best -= lowestQuiz.getPoints();
}
// Print out late and resubmitted assignments
pw.println("Late assignments:");
for (String late : student.getLate()) {
pw.println(" " + late);
}
pw.println();
pw.println("Resubmitted assignments:");
for (String resubmitted : student.getResubmitted()) {
pw.println(" " + resubmitted);
}
pw.println("");
pw.println("Total grade: " + format.format(total) + "/" +
format.format(best));
double overallScore = total / best;
if (assignLetterGrades) {
LetterGrade letterGrade = book.getLetterGradeForScore(student.getEnrolledSection(), overallScore * 100.0);
student.setLetterGrade(letterGrade);
}
if (student.getLetterGrade() != null) {
pw.println("Letter Grade: " + student.getLetterGrade());
}
allTotals.put(student, overallScore);
}
static boolean noStudentHasGradeFor(Assignment assignment, GradeBook book) {
return book.studentsStream().noneMatch(student -> studentHasGradeFor(student, assignment));
}
private static boolean studentHasGradeFor(Student student, Assignment assignment) {
Grade grade = student.getGrade(assignment.getName());
return grade != null && !grade.isNotGraded();
}
private static double getScore(Grade grade) {
// Average non-existent scores as zero
double score;
if (grade == null) {
score = 0.0;
} else {
score = grade.getScore();
}
return score;
}
private static SortedSet<String> getSortedAssignmentNames(GradeBook book) {
return new TreeSet<>(book.getAssignmentNames());
}
private static PrintWriter out = new PrintWriter(System.out, true);
private static PrintWriter err = new PrintWriter(System.err, true);
/**
* Prints usage information about this main program
*/
private static void usage(String s) {
err.println("\n** " + s + "\n");
err.println("\njava SummaryReport -assignLetterGrades xmlFile outDir (student)*");
err.println("\n");
err.println("Generates summary grade reports for the given " +
"students. If student is not \ngiven, then reports " +
"for all students are generated.");
err.println("");
System.exit(1);
}
/**
* Main program that creates summary reports for every student in a
* grade book located in a given XML file.
*/
public static void main(String[] args) {
boolean assignLetterGrades = false;
String xmlFileName = null;
String outputDirName = null;
Collection<String> students = new ArrayList<>();
for (String arg : args) {
if (arg.equals("-assignLetterGrades")) {
assignLetterGrades = true;
} else if (xmlFileName == null) {
xmlFileName = arg;
} else if (outputDirName == null) {
outputDirName = arg;
} else {
students.add(arg);
}
}
if (xmlFileName == null) {
usage("Missing XML file name");
}
if (outputDirName == null) {
usage("Missing output dir name");
return;
}
String xmlFile = xmlFileName;
File outDir = new File(outputDirName);
if (!outDir.exists()) {
outDir.mkdirs();
} else if (!outDir.isDirectory()) {
usage(outDir + " is not a directory");
}
File file = new File(xmlFile);
if (!file.exists()) {
usage("Grade book file " + xmlFile + " does not exist");
}
GradeBook book = parseGradeBook(file);
// Create a SummaryReport for every student
Iterable<String> studentIds;
if (!students.isEmpty()) {
studentIds = students;
} else {
studentIds = book.getStudentIds();
}
dumpReports(studentIds, book, outDir, assignLetterGrades);
// Sort students by totals and print out results:
Set<Student> students1 = allTotals.keySet();
printOutStudentTotals(students1, out);
saveGradeBookIfDirty(xmlFileName, book);
}
@VisibleForTesting
static void printOutStudentTotals(Set<Student> allStudents, PrintWriter out) {
SortedSet<Student> sorted = getStudentSortedByTotalPoints(allStudents);
out.println("Undergraduates:");
Stream<Student> undergrads = sorted.stream().filter(student -> student.getEnrolledSection() == UNDERGRADUATE);
printOutStudentTotals(out, undergrads);
out.println("Graduate Students:");
Stream<Student> grads = sorted.stream().filter(student -> student.getEnrolledSection() == GRADUATE);
printOutStudentTotals(out, grads);
}
private static void printOutStudentTotals(PrintWriter out, Stream<Student> students) {
NumberFormat format = NumberFormat.getPercentInstance();
students.forEach(student -> {
Double d = allTotals.get(student);
out.print(" " + student + ": " + format.format(d.doubleValue()));
if (student.getLetterGrade() != null) {
out.print(" " + student.getLetterGrade());
}
out.println();
});
}
private static void saveGradeBookIfDirty(String xmlFileName, GradeBook book) {
if (book.isDirty()) {
try {
XmlDumper dumper = new XmlDumper(xmlFileName);
dumper.dump(book);
} catch (IOException ex) {
printErrorMessageAndExit("While saving gradebook to " + xmlFileName, ex);
}
}
}
private static SortedSet<Student> getStudentSortedByTotalPoints(Set<Student> students) {
SortedSet<Student> sorted = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
Double d1 = allTotals.get(s1);
Double d2 = allTotals.get(s2);
if ( d2.compareTo( d1 ) == 0 ) {
return s1.getId().compareTo( s2.getId() );
} else {
return d2.compareTo(d1);
}
}
@Override
public boolean equals(Object o) {
return true;
}
});
sorted.addAll(students);
return sorted;
}
@VisibleForTesting
static void dumpReports(Iterable<String> studentIds, GradeBook book, File outDir, boolean assignLetterGrades) {
for (String id : studentIds) {
err.println(id);
Student student = book.getStudent(id).orElseThrow(noStudentWithId(id));
File outFile = new File(getDirectoryForReportFileForStudent(outDir, student), getReportFileName(id));
try {
PrintWriter pw =
new PrintWriter(new FileWriter(outFile), true);
dumpReportTo(book, student, pw, assignLetterGrades);
// dumpReportTo(book, student, out);
} catch (IOException ex) {
printErrorMessageAndExit("While writing report to " + outFile, ex);
}
}
}
private static File getDirectoryForReportFileForStudent(File parentDirectory, Student student) {
String directoryName;
Student.Section enrolledSection = student.getEnrolledSection();
switch (enrolledSection) {
case UNDERGRADUATE:
directoryName = UNDERGRADUATE_DIRECTORY_NAME;
break;
case GRADUATE:
directoryName = GRADUATE_DIRECTORY_NAME;
break;
default:
throw new IllegalStateException("Don't know directory name for " + enrolledSection);
}
File directory = new File(parentDirectory, directoryName);
directory.mkdirs();
return directory;
}
@VisibleForTesting
static String getReportFileName(String studentId) {
return studentId + ".report";
}
private static GradeBook parseGradeBook(File gradeBookFile) {
GradeBook book = null;
try {
err.println("Parsing " + gradeBookFile);
XmlGradeBookParser parser = new XmlGradeBookParser(gradeBookFile);
book = parser.parse();
} catch (FileNotFoundException ex) {
printErrorMessageAndExit("** Could not find grade book file: " + gradeBookFile, ex);
} catch (ParserException | IOException ex) {
printErrorMessageAndExit("** Exception while parsing " + gradeBookFile, ex);
}
return book;
}
private static void printErrorMessageAndExit(String message, Throwable ex) {
err.println(message);
System.exit(1);
}
@SuppressWarnings("ThrowableInstanceNeverThrown")
private static Supplier<? extends IllegalStateException> noStudentWithId(String id) {
return () -> new IllegalStateException("No student with id " + id);
}
}