001package edu.pdx.cs.joy.grader.gradebook;
002
003import edu.pdx.cs.joy.ParserException;
004
005import java.io.File;
006import java.io.FileNotFoundException;
007import java.io.IOException;
008import java.io.PrintWriter;
009import java.util.*;
010import java.util.function.Consumer;
011import java.util.stream.Stream;
012
013import static edu.pdx.cs.joy.grader.gradebook.GradeBook.LetterGradeRanges.LetterGradeRange;
014
015/**
016 * This class represents a grade book that contains information about
017 * an offering of the course: the assignments, the students and their grades.
018 *
019 * @author David Whitlock
020 * @since Fall 2000
021 */
022public class GradeBook {
023
024  /** The name of the class */
025  private String className;
026
027  /** Maps the name of an assignment to an <code>Assignment</code> */
028  private Map<String, Assignment> assignments = new TreeMap<>();
029
030  /** Maps the id of a student to a <code>Student</code> object */
031  private Map<String, Student> students = new TreeMap<>();
032
033  /** Has the grade book been modified? */
034  private boolean dirty = true;
035
036  /** The Course Request Number (CRN) for this gradebook */
037  private int crn = 0;
038
039  private final Map<Student.Section, LetterGradeRanges> letterGradeRanges = new HashMap<>();
040  private final Map<Student.Section, String> sectionNames = new HashMap<>();
041
042  /**
043   * Creates a new <code>GradeBook</code> for a given class
044   */
045  public GradeBook(String className) {
046    this.className = className;
047    for(Student.Section section : getSections()) {
048      letterGradeRanges.put(section, new LetterGradeRanges());
049    }
050  }
051
052  public Iterable<Student.Section> getSections() {
053    return Arrays.asList(Student.Section.UNDERGRADUATE, Student.Section.GRADUATE);
054  }
055
056  /**
057   * Returns the name of the class represented by this
058   * <code>GradeBook</code>
059   */
060  public String getClassName() {
061    return this.className;
062  }
063
064  /**
065   * Returns the names of the assignments for this class
066   */
067  public Set<String> getAssignmentNames() {
068    return this.assignments.keySet();
069  }
070
071  /**
072   * Sets the Course Request Number (CRN) for this grade book.
073   *
074   * @since Spring 2005
075   */
076  public void setCRN(int crn) {
077    this.setDirty(true);
078    this.crn = crn;
079  }
080
081  /**
082   * Returns the Course Request Number (CRN) for this grade book.
083   *
084   * @since Spring 2005
085   */
086  public int getCRN() {
087    return this.crn;
088  }
089
090  /**
091   * Returns the <code>Assignment</code> of a given name
092   */
093  public Assignment getAssignment(String name) {
094    return this.assignments.get(name);
095  }
096
097  /**
098   * Adds an <code>Assignment</code> to this class
099   *
100   * @return <code>assignment</code> so that method calls can be chained
101   */
102  public Assignment addAssignment(Assignment assignment) {
103    this.setDirty(true);
104    this.assignments.put(assignment.getName(), assignment);
105    return assignment;
106  }
107
108  /**
109   * Returns the ids of all the students in this class
110   */
111  public Set<String> getStudentIds() {
112    return this.students.keySet();
113  }
114
115  public Optional<Student> getStudent(String id) {
116    return Optional.ofNullable(this.students.get(id));
117  }
118
119  public Stream<Student> studentsStream() {
120    return this.students.values().stream();
121  }
122
123  /**
124   * Adds a <code>Student</code> to this <code>GradeBook</code>
125   *
126   * @return <code>student</code> so that calls can be chained
127   * @see #containsStudent
128   */
129  public Student addStudent(Student student) {
130    this.setDirty(true);
131    this.students.put(student.getId(), student);
132    return student;
133  }
134
135  /**
136   * Removes a <code>Student</code> from this <code>GradeBook</code>
137   */
138  public void removeStudent(Student student) {
139    if (this.students.remove(student.getId()) != null) {
140      this.setDirty(true);
141    }
142  }
143
144  /**
145   * Returns whether or not this grade book contains a student with
146   * the given id.
147   */
148  public boolean containsStudent(String id) {
149    return this.students.containsKey(id);
150  }
151
152  /**
153   * Sets the dirtiness of this <code>GradeBook</code>
154   */
155  public void setDirty(boolean dirty) {
156    this.dirty = dirty;
157  }
158
159  /**
160   * Marks this <code>GradeBook</code> as being clean
161   */
162  public void makeClean() {
163    this.setDirty(false);
164
165    for (Assignment assignment : this.assignments.values()) {
166      assignment.makeClean();
167    }
168
169    for (Student student : this.students.values()) {
170      student.makeClean();
171    }
172  }
173
174  /**
175   * Returns <code>true</code> if this <code>GradeBook</code> has been
176   * modified.
177   */
178  public boolean isDirty() {
179    if (this.dirty) {
180      return true;
181    }
182
183    // Are any of the Assignments dirty?
184    Iterator iter = this.assignments.values().iterator();
185    while (iter.hasNext()) {
186      Assignment assign = (Assignment) iter.next();
187      if (assign.isDirty()) {
188        return true;
189      }
190    }
191
192    // Are any of the Students dirty?
193    iter = this.students.values().iterator();
194    while (iter.hasNext()) {
195      Student student = (Student) iter.next();
196      if (student.isDirty()) {
197        return true;
198      }
199    }
200
201    return false;
202  }
203
204  /**
205   * Returns a brief textual description of this
206   * <code>GradeBook</code>.
207   */
208  public String toString() {
209    return "Grade book for " + this.getClassName() + " with " +
210           this.getStudentIds().size() + " students";
211  }
212
213  private static PrintWriter err = new PrintWriter(System.err, true);
214
215  /**
216   * Prints usage information about the main program.
217   */
218  private static void usage() {
219    err.println("\nusage: java GradeBook -file xmlFile [options]");
220    err.println("Where [options] are:");
221    err.println("  -name className      Create new class file");
222    err.println("  -import xmlName      Import a student from a file");
223    err.println("\n");
224    System.exit(1);
225  }
226
227  /**
228   * Main program that is used to create a <code>GradeBook</code>
229   */
230  public static void main(String[] args) {
231    String xmlFile = null;
232    String name = null;
233    String importName = null;
234
235    // Parse the command line
236    for (int i = 0; i < args.length; i++) {
237      if (args[i].equals("-file")) {
238        if (++i >= args.length) {
239          err.println("** Missing file name");
240          usage();
241        }
242
243        xmlFile = args[i];
244
245      } else if (args[i].equals("-name")) {
246        if (++i >= args.length) {
247          err.println("** Missing class name");
248          usage();
249        }
250
251        name = args[i];
252
253      } else if (args[i].equals("-import")) {
254        if (++i >= args.length) {
255          err.println("** Missing import file name");
256          usage();
257        }
258
259        importName = args[i];
260
261      } else {
262        err.println("** Spurious command line option: " + args[i]);
263        usage();
264      }
265    }
266
267    if (xmlFile == null) {
268      err.println("** No XML file specified");
269      usage();
270    }
271
272    GradeBook book = null;
273
274    File file = new File(xmlFile);
275    if (file.exists()) {
276      // Parse a grade book from the XML file
277      try {
278        XmlGradeBookParser parser = new XmlGradeBookParser(file);
279        book = parser.parse();
280
281      } catch (FileNotFoundException ex) {
282        err.println("** Could not find file: " + ex.getMessage());
283        System.exit(1);
284
285      } catch (IOException ex) {
286        err.println("** IOException during parsing: " + ex.getMessage());
287        System.exit(1);
288
289      } catch (ParserException ex) {
290        err.println("** Error during parsing: " + ex.getMessage());
291        System.exit(1);
292      }
293
294      // Do we import?
295      if (importName != null) {
296        File importFile = new File(importName);
297        if (!importFile.exists()) {
298          err.println("** Import file " + importFile.getName() + 
299                      " does not exist");
300          System.exit(1);
301        }
302
303        try {
304          XmlStudentParser sp = new XmlStudentParser(importFile);
305          Student student = sp.parseStudent();
306          book.addStudent(student);
307
308        } catch (IOException ex) {
309          err.println("** Error during parsing: " + ex.getMessage());
310          System.exit(1);
311
312        } catch (ParserException ex) {
313          err.println("** Error during parsing: " + ex.getMessage());
314          System.exit(1);
315        }
316      }
317
318    } else if (name == null) {
319      err.println("** Must specify a class name when creating a " +
320                  "grade book");
321      System.exit(1);
322      
323    } else {
324      // Create an empty GradeBook
325      book = new GradeBook(name);
326    }
327
328    // Write the grade book to the XML file
329    try {
330      XmlDumper dumper = new XmlDumper(file);
331      dumper.dump(book);
332
333    } catch (IOException ex) {
334      err.println("** Error while writing XML file: " + ex);
335      System.exit(1);
336    }
337  }
338
339  public LetterGradeRanges getLetterGradeRanges(Student.Section section) {
340    return letterGradeRanges.get(section);
341  }
342
343  public LetterGrade getLetterGradeForScore(Student.Section section, double score) {
344    for (LetterGradeRange range : this.getLetterGradeRanges(section)) {
345      if (range.isScoreInRange(score)) {
346        return range.letterGrade();
347      }
348    }
349
350    throw new IllegalStateException("Could not find a letter grade range for " + score);
351  }
352
353  public void forEachStudent(Consumer<Student> consumer) {
354    this.students.values().forEach(consumer);
355  }
356
357  public Stream<Assignment> assignmentsStream() {
358    return this.assignments.values().stream();
359  }
360
361  public String getSectionName(Student.Section section) {
362    return this.sectionNames.get(section);
363  }
364
365  public void setSectionName(Student.Section section, String sectionName) {
366    this.sectionNames.put(section, sectionName);
367  }
368
369  public static class LetterGradeRanges implements Iterable<LetterGradeRanges.LetterGradeRange> {
370    private final Map<LetterGrade, LetterGradeRange> ranges = new TreeMap<>();
371
372    private LetterGradeRanges() {
373      createDefaultLetterGradeRange(LetterGrade.A, 94, 100);
374      createDefaultLetterGradeRange(LetterGrade.A_MINUS, 90, 93);
375      createDefaultLetterGradeRange(LetterGrade.B_PLUS, 87, 89);
376      createDefaultLetterGradeRange(LetterGrade.B, 83, 86);
377      createDefaultLetterGradeRange(LetterGrade.B_MINUS, 80, 82);
378      createDefaultLetterGradeRange(LetterGrade.C_PLUS, 77, 79);
379      createDefaultLetterGradeRange(LetterGrade.C, 73, 76);
380      createDefaultLetterGradeRange(LetterGrade.C_MINUS, 70, 72);
381      createDefaultLetterGradeRange(LetterGrade.D_PLUS, 67, 69);
382      createDefaultLetterGradeRange(LetterGrade.D, 63, 66);
383      createDefaultLetterGradeRange(LetterGrade.D_MINUS, 60, 62);
384      createDefaultLetterGradeRange(LetterGrade.F, 0, 59);
385    }
386
387    private void createDefaultLetterGradeRange(LetterGrade letterGrade, int minimum, int maximum) {
388      this.ranges.put(letterGrade, new LetterGradeRange(letterGrade, minimum, maximum));
389    }
390
391    public LetterGradeRange getRange(LetterGrade letterGrade) {
392      return ranges.get(letterGrade);
393    }
394
395    public void validate() {
396      validateThatFRangeHasAMinimumOf0();
397      validateThatARangeContains100();
398      validateThatRangesAreContiguous();
399    }
400
401    private void validateThatRangesAreContiguous() {
402      LetterGradeRange previous = null;
403
404      for (LetterGrade letterGrade : ranges.keySet()) {
405        LetterGradeRange current = ranges.get(letterGrade);
406        if (previous != null) {
407          if (previous.minimum() != current.maximum() + 1) {
408            String s = "There is a gap between the range for " + previous + " and " + current;
409            throw new LetterGradeRange.InvalidLetterGradeRange(s);
410          }
411        }
412
413        previous = current;
414      }
415
416    }
417
418    private void validateThatARangeContains100() {
419      LetterGradeRange range = getRange(LetterGrade.A);
420      if (range.maximum() < 100) {
421        throw new LetterGradeRange.InvalidLetterGradeRange("The A range must contain 100");
422      }
423    }
424
425    private void validateThatFRangeHasAMinimumOf0() {
426      LetterGradeRange range = getRange(LetterGrade.F);
427      if (range.minimum() > 0) {
428        throw new LetterGradeRange.InvalidLetterGradeRange("The F range must contain zero");
429      }
430    }
431
432    @Override
433    public Iterator<LetterGradeRange> iterator() {
434      return this.ranges.values().iterator();
435    }
436
437    @Override
438    public void forEach(Consumer<? super LetterGradeRange> action) {
439      this.ranges.values().forEach(action);
440    }
441
442    @Override
443    public Spliterator<LetterGradeRange> spliterator() {
444      return this.ranges.values().spliterator();
445    }
446
447    public static class LetterGradeRange {
448      private final LetterGrade letterGrade;
449      private int maximum;
450      private int minimum;
451
452      public LetterGradeRange(LetterGrade letterGrade, int minimum, int maximum) {
453        this.letterGrade = letterGrade;
454        setRange(minimum, maximum);
455      }
456
457      public void setRange(int minimum, int maximum) {
458        if (minimum >= maximum) {
459          String s = String.format("Minimum value (%d) must be less than maximum value (%d)",
460            minimum, maximum);
461          throw new InvalidLetterGradeRange(s);
462        }
463
464        this.minimum = minimum;
465        this.maximum = maximum;
466      }
467
468      public int minimum() {
469        return this.minimum;
470      }
471
472      public int maximum() {
473        return this.maximum;
474      }
475
476      @Override
477      public String toString() {
478        return "Range for " + letterGrade() + " is " + minimum() + " to " + maximum();
479      }
480
481      public LetterGrade letterGrade() {
482        return this.letterGrade;
483      }
484
485      public boolean isScoreInRange(double score) {
486        int intScore = (int) Math.round(score);
487        return intScore >= minimum() && intScore <= maximum();
488      }
489
490      public static class InvalidLetterGradeRange extends RuntimeException {
491        public InvalidLetterGradeRange(String message) {
492          super(message);
493        }
494      }
495    }
496  }
497}