package edu.pdx.cs410J.grader; import edu.pdx.cs410J.ParserException; import edu.pdx.cs410J.grader.Student.Section; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.util.*; import java.util.function.Consumer; import java.util.stream.Stream; import static edu.pdx.cs410J.grader.GradeBook.LetterGradeRanges.LetterGradeRange; /** * This class represents a grade book that contains information about * a CS410J class: the assignments, the students and their grades. * * @author David Whitlock * @since Fall 2000 */ public class GradeBook { /** The name of the class */ private String className; /** Maps the name of an assignment to an <code>Assignment</code> */ private Map<String, Assignment> assignments = new TreeMap<>(); /** Maps the id of a student to a <code>Student</code> object */ private Map<String, Student> students = new TreeMap<>(); /** Has the grade book been modified? */ private boolean dirty = true; /** The Course Request Number (CRN) for this gradebook */ private int crn = 0; private final Map<Section, LetterGradeRanges> letterGradeRanges = new HashMap<>(); /** * Creates a new <code>GradeBook</code> for a given class */ public GradeBook(String className) { this.className = className; for(Section section : getSections()) { letterGradeRanges.put(section, new LetterGradeRanges()); } } public Iterable<Section> getSections() { return Arrays.asList(Section.UNDERGRADUATE, Section.GRADUATE); } /** * Returns the name of the class represented by this * <code>GradeBook</code> */ public String getClassName() { return this.className; } /** * Returns the names of the assignments for this class */ public Set<String> getAssignmentNames() { return this.assignments.keySet(); } /** * Sets the Course Request Number (CRN) for this grade book. * * @since Spring 2005 */ public void setCRN(int crn) { this.setDirty(true); this.crn = crn; } /** * Returns the Course Request Number (CRN) for this grade book. * * @since Spring 2005 */ public int getCRN() { return this.crn; } /** * Returns the <code>Assignment</code> of a given name */ public Assignment getAssignment(String name) { return this.assignments.get(name); } /** * Adds an <code>Assignment</code> to this class */ public void addAssignment(Assignment assign) { this.setDirty(true); this.assignments.put(assign.getName(), assign); } /** * Returns the ids of all of the students in this class */ public Set<String> getStudentIds() { return this.students.keySet(); } public Optional<Student> getStudent(String id) { return Optional.ofNullable(this.students.get(id)); } public Stream<Student> studentsStream() { return this.students.values().stream(); } /** * Adds a <code>Student</code> to this <code>GradeBook</code> * * @see #containsStudent */ public void addStudent(Student student) { this.setDirty(true); this.students.put(student.getId(), student); } /** * Removes a <code>Student</code> from this <code>GradeBook</code> */ public void removeStudent(Student student) { if (this.students.remove(student.getId()) != null) { this.setDirty(true); } } /** * Returns whether or not this grade book contains a student with * the given id. */ public boolean containsStudent(String id) { return this.students.containsKey(id); } /** * Sets the dirtiness of this <code>GradeBook</code> */ public void setDirty(boolean dirty) { this.dirty = dirty; } /** * Marks this <code>GradeBook</code> as being clean */ public void makeClean() { this.setDirty(false); for (Assignment assignment : this.assignments.values()) { assignment.makeClean(); } for (Student student : this.students.values()) { student.makeClean(); } } /** * Returns <code>true</code> if this <code>GradeBook</code> has been * modified. */ public boolean isDirty() { if (this.dirty) { return true; } // Are any of the Assignments dirty? Iterator iter = this.assignments.values().iterator(); while (iter.hasNext()) { Assignment assign = (Assignment) iter.next(); if (assign.isDirty()) { return true; } } // Are any of the Students dirty? iter = this.students.values().iterator(); while (iter.hasNext()) { Student student = (Student) iter.next(); if (student.isDirty()) { return true; } } return false; } /** * Returns a brief textual description of this * <code>GradeBook</code>. */ public String toString() { return "Grade book for " + this.getClassName() + " with " + this.getStudentIds().size() + " students"; } private static PrintWriter err = new PrintWriter(System.err, true); /** * Prints usage information about the main program. */ private static void usage() { err.println("\nusage: java GradeBook -file xmlFile [options]"); err.println("Where [options] are:"); err.println(" -name className Create new class file"); err.println(" -import xmlName Import a student from a file"); err.println("\n"); System.exit(1); } /** * Main program that is used to create a <code>GradeBook</code> */ public static void main(String[] args) { String xmlFile = null; String name = null; String importName = null; // Parse the command line for (int i = 0; i < args.length; i++) { if (args[i].equals("-file")) { if (++i >= args.length) { err.println("** Missing file name"); usage(); } xmlFile = args[i]; } else if (args[i].equals("-name")) { if (++i >= args.length) { err.println("** Missing class name"); usage(); } name = args[i]; } else if (args[i].equals("-import")) { if (++i >= args.length) { err.println("** Missing import file name"); usage(); } importName = args[i]; } else { err.println("** Spurious command line option: " + args[i]); usage(); } } if (xmlFile == null) { err.println("** No XML file specified"); usage(); } GradeBook book = null; File file = new File(xmlFile); if (file.exists()) { // Parse a grade book from the XML file try { XmlGradeBookParser parser = new XmlGradeBookParser(file); book = parser.parse(); } catch (FileNotFoundException ex) { err.println("** Could not find file: " + ex.getMessage()); System.exit(1); } catch (IOException ex) { err.println("** IOException during parsing: " + ex.getMessage()); System.exit(1); } catch (ParserException ex) { err.println("** Error during parsing: " + ex.getMessage()); System.exit(1); } // Do we import? if (importName != null) { File importFile = new File(importName); if (!importFile.exists()) { err.println("** Import file " + importFile.getName() + " does not exist"); System.exit(1); } try { XmlStudentParser sp = new XmlStudentParser(importFile); Student student = sp.parseStudent(); book.addStudent(student); } catch (IOException ex) { err.println("** Error during parsing: " + ex.getMessage()); System.exit(1); } catch (ParserException ex) { err.println("** Error during parsing: " + ex.getMessage()); System.exit(1); } } } else if (name == null) { err.println("** Must specify a class name when creating a " + "grade book"); System.exit(1); } else { // Create an empty GradeBook book = new GradeBook(name); } // Write the grade book to the XML file try { XmlDumper dumper = new XmlDumper(file); dumper.dump(book); } catch (IOException ex) { err.println("** Error while writing XML file: " + ex); System.exit(1); } } public LetterGradeRanges getLetterGradeRanges(Section section) { return letterGradeRanges.get(section); } public LetterGrade getLetterGradeForScore(Section section, double score) { for (LetterGradeRange range : this.getLetterGradeRanges(section)) { if (range.isScoreInRange(score)) { return range.letterGrade(); } } throw new IllegalStateException("Could not find a letter grade range for " + score); } public void forEachStudent(Consumer<Student> consumer) { this.students.values().forEach(consumer); } public Stream<Assignment> assignmentsStream() { return this.assignments.values().stream(); } public Optional<Student> getStudentWithSsn(String ssn) { return this.studentsStream().filter(s -> ssn.equals(s.getSsn())).findAny(); } static class LetterGradeRanges implements Iterable<LetterGradeRanges.LetterGradeRange> { private final Map<LetterGrade, LetterGradeRange> ranges = new TreeMap<>(); private LetterGradeRanges() { createDefaultLetterGradeRange(LetterGrade.A, 94, 100); createDefaultLetterGradeRange(LetterGrade.A_MINUS, 90, 93); createDefaultLetterGradeRange(LetterGrade.B_PLUS, 87, 89); createDefaultLetterGradeRange(LetterGrade.B, 83, 86); createDefaultLetterGradeRange(LetterGrade.B_MINUS, 80, 82); createDefaultLetterGradeRange(LetterGrade.C_PLUS, 77, 79); createDefaultLetterGradeRange(LetterGrade.C, 73, 76); createDefaultLetterGradeRange(LetterGrade.C_MINUS, 70, 72); createDefaultLetterGradeRange(LetterGrade.D_PLUS, 67, 69); createDefaultLetterGradeRange(LetterGrade.D, 63, 66); createDefaultLetterGradeRange(LetterGrade.D_MINUS, 60, 62); createDefaultLetterGradeRange(LetterGrade.F, 0, 59); } private void createDefaultLetterGradeRange(LetterGrade letterGrade, int minimum, int maximum) { this.ranges.put(letterGrade, new LetterGradeRange(letterGrade, minimum, maximum)); } public LetterGradeRange getRange(LetterGrade letterGrade) { return ranges.get(letterGrade); } public void validate() { validateThatFRangeHasAMinimumOf0(); validateThatARangeContains100(); validateThatRangesAreContiguous(); } private void validateThatRangesAreContiguous() { LetterGradeRange previous = null; for (LetterGrade letterGrade : ranges.keySet()) { LetterGradeRange current = ranges.get(letterGrade); if (previous != null) { if (previous.minimum() != current.maximum() + 1) { String s = "There is a gap between the range for " + previous + " and " + current; throw new LetterGradeRange.InvalidLetterGradeRange(s); } } previous = current; } } private void validateThatARangeContains100() { LetterGradeRange range = getRange(LetterGrade.A); if (range.maximum() < 100) { throw new LetterGradeRange.InvalidLetterGradeRange("The A range must contain 100"); } } private void validateThatFRangeHasAMinimumOf0() { LetterGradeRange range = getRange(LetterGrade.F); if (range.minimum() > 0) { throw new LetterGradeRange.InvalidLetterGradeRange("The F range must contain zero"); } } @Override public Iterator<LetterGradeRange> iterator() { return this.ranges.values().iterator(); } @Override public void forEach(Consumer<? super LetterGradeRange> action) { this.ranges.values().forEach(action); } @Override public Spliterator<LetterGradeRange> spliterator() { return this.ranges.values().spliterator(); } static class LetterGradeRange { private final LetterGrade letterGrade; private int maximum; private int minimum; public LetterGradeRange(LetterGrade letterGrade, int minimum, int maximum) { this.letterGrade = letterGrade; setRange(minimum, maximum); } public void setRange(int minimum, int maximum) { if (minimum >= maximum) { String s = String.format("Minimum value (%d) must be less than maximum value (%d)", minimum, maximum); throw new InvalidLetterGradeRange(s); } this.minimum = minimum; this.maximum = maximum; } public int minimum() { return this.minimum; } public int maximum() { return this.maximum; } @Override public String toString() { return "Range for " + letterGrade() + " is " + minimum() + " to " + maximum(); } public LetterGrade letterGrade() { return this.letterGrade; } public boolean isScoreInRange(double score) { int intScore = (int) Math.round(score); return intScore >= minimum() && intScore <= maximum(); } public static class InvalidLetterGradeRange extends RuntimeException { public InvalidLetterGradeRange(String message) { super(message); } } } } }