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}