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