-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGrader.java
2462 lines (2025 loc) · 96.4 KB
/
Grader.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import java.util.Scanner;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.*;
import java.nio.file.attribute.*;
//import java.nio.file.Files;
//import java.nio.file.Paths;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.regex.Pattern;
import java.text.DecimalFormat;
/**
Grader.java
A program that automates grading by checking student solution files
against a master solution file. Grader allows for the creation of
automatic responses and response files, for flexibility in grading,
for optimization in creating solution files and for creating student
response templates, for pattern recognition of mistakes made across
questions within an assignment.
@author Peter Olson
@version 07/22/22 v1.0
*/
public class Grader {
private static Scanner scanner = new Scanner( System.in );
private static final String GRADER_SETTINGS_FILE_NAME = "Grader_Settings.txt";
/*Grader Settings -- @@NOTE: Anytime a setting is added, it needs to be added in the setGraderSettings()
method, the Grader_Settings.txt file, and the global variables below*/
private static boolean spacesMatter = false;
private static boolean useAutoAltSolutions = true;
private static int defaultPointValue = 1;
private static boolean manuallyEnterSolutions = false;
private static boolean listOrderMatters = true;
private static boolean convertFractionsToDecimals = true;
private static boolean createResultsFile = true;
private static boolean includeAlgebraicAlternates = true;
/**
Run the menu options
@see printMenu()
@see setGraderSettings()
@see gradeAllTextFiles()
@see gradeTextFile()
@see changeGradingOptions()
@see createSolutionFile()
@see editSolutionFile()
@see createStudentResponseTemplate()
@see checkForPlagiarism()
@see generateStatistics()
@see editAllFiles()
@see editResponseFile()
@see printFile()
*/
public static void main( String[] args ) {
String response = "";
printMenu( false );
do {
setGraderSettings();
response = scanner.nextLine().toLowerCase();
char option = response.charAt(0);
switch(option) {
case 'a': gradeAllTextFiles(); break;
case 'b': gradeTextFile(); break;
case 'c': changeGradingOptions(); break;
case 'd': createSolutionFile(); break;
case 'e': editSolutionFile(); break;
case 'f': createStudentResponseTemplate(); break;
case 'g': checkForPlagiarism(); break;
case 'h': generateStatistics(); break;
case 'i': editAllFiles(); break;
case 'j': editResponseFile(); break;
case 'k': printFile(); break;
case 'l': reformatSolutionFile(); break;
case 'm': retrieveDownloadedFiles(); break;
case 'q': SOPln("\nGoodbye."); break;
default : SOPln("\nPlease enter a letter.\n"); break;
}
if( !response.equals("q") && !response.contains("quit") )
printMenu( true );
} while( !response.equals("q") && !response.contains("quit") );
}
/**
Grade all student response text files by comparing the answers in each
to the solution text file. The files are graded according to the grading
options text file and the alternate solutions within the solutions text file.
Any answers found to be incorrect (or if they received partial credit) are
then listed in a new response text file that includes the grades and points lost
for that student, along with any automated responses based on the incorrect value
for each question (these are options and are set within the solutions file).
@see gradeTextFile( File file )
*/
public static void gradeAllTextFiles() {
SOPln("\nWhat group of files do you want to edit?\n" +
"(Enter the identifying name of this group, such as\n" +
"\"HW6\" or \"Quiz2\"\n");
String inclusionToken = scanner.nextLine();
File[] studentFiles = getTextFiles( new String[]{"Temp","Settings","Solution"} );
File solutionFile = getSolutionFile();
//@@DEBUG
//SOPln("\nThe solution file found is: " + solutionFile.getName() + "\n");
for( File studentFile : studentFiles )
if( studentFile.getName().toLowerCase().contains( removeWhitespace( inclusionToken.toLowerCase() ) ) )
gradeTextFile( studentFile, solutionFile );
}
/**
Returns the list of text files in the current directory
@return File[] The list of text files in the current directory
*/
private static File[] getTextFiles() {
return getTextFiles( new String[]{} );
}
/**
Returns the list of text files in the current directory, excluding files whose names contain the given String
@param exclusion The token to check against the names of the Files for the purposes of
excluding those files
@return File[] The list of text files in the current directory
*/
private static File[] getTextFiles( String exclusion ) {
return getTextFiles( new String[]{ exclusion } );
}
/**
Returns the list of text files in the current directory, excluding files whose names contain any of the tokens
in the list
@param exclusionList The list of tokens to check against the names of the Files for the purposes of
excluding those files
@return File[] The list of text files in the current directory
*/
private static File[] getTextFiles( String[] exclusionList ) {
File dir = new File(".");
File[] filesList = dir.listFiles();
ArrayList<File> newFileList = new ArrayList<File>();
for( File file : filesList ) {
if( file.isFile() ) {
boolean willInclude = true;
if( file.toString().contains("txt") && !file.toString().contains("Grader_Settings.txt") ) {
for( int i = 0; i < exclusionList.length; i++ ) {
willInclude &= !file.toString().toLowerCase().contains( exclusionList[i].toLowerCase() );
}
} else {
willInclude = false;
}
if( willInclude ) newFileList.add( file );
}
}
File[] newFileListArray = new File[ newFileList.size() ];
//convert to array since toArray() does not preserve order
for( int i = 0; i < newFileList.size(); i++ )
newFileListArray[i] = newFileList.get(i);
return newFileListArray;
}
/**
Gets the correct solution text file.
If the current directory has no text files that contain the
word 'solution', then 'null' is returned and the user is
informed that there are no solution files in the current
directory.
If the current directory has just one solution file, this
file is automatically returned
If the current directory has multiple solution files, the
user is asked which solution file that they want to use
@return File The name of the solution text file that will be used
@see gradeAllTextFiles()
*/
private static File getSolutionFile() {
File[] fileList = getTextFiles();
int solutionTextFileCounter = 0;
int index = -1;
ArrayList<Integer> solutionFileIndices = new ArrayList<Integer>();
for( int i = 0; i < fileList.length; i++ ) {
if( fileList[i].getName().toLowerCase().contains("solution") ) {
solutionTextFileCounter++;
index = i;
solutionFileIndices.add( index );
}
}
if( solutionTextFileCounter == 0 ) {
SOPln("\nNo solution files found.\n");
return null;
}
if( solutionTextFileCounter == 1 )
return fileList[index];
//Slim down file list to just the solution files
ArrayList<File> solutionFiles = new ArrayList<File>();
int totalSolutionFiles = solutionFileIndices.size();
for( int i = 0; i < totalSolutionFiles; i++ )
solutionFiles.add( fileList[ solutionFileIndices.get(i) ] );
boolean fileFound = false;
do {
printFileListByName( solutionFiles );
SOPln("\nWhich solution file?");
String response = scanner.nextLine().toLowerCase();
if( response.equals("quit") ) break;
//If enter number index
boolean invalidIndex = false;
if( isNumeric( response.trim().replaceAll("\\.", "" ) ) ) {
try {
response = response.trim().replaceAll("\\.", "" );
int indexPos = Integer.parseInt( response ) - 1;
return solutionFiles.get( indexPos );
} catch( NumberFormatException e ) {
//treat as a name entered, skip exception
} catch( IndexOutOfBoundsException e ) {
SOPln("\nInvalid index. Please enter a number between 1 and " + totalSolutionFiles );
invalidIndex = true;
}
}
if( !response.contains(".txt") ) response += ".txt";
//if enter the name of the file ^ v
if( !invalidIndex )
for( int i = 0; i < totalSolutionFiles; i++ ) {
if( response.equals( solutionFiles.get(i).getName().toLowerCase() ) )
return solutionFiles.get(i);
}
if( !invalidIndex ) SOPln("\nFile not found. Try again.");
} while( !fileFound ); //always true if reaches this point
return null; //should not be reached
}
/**
Gets the correct student file based on the name
If the current directory has no text files that contain the
name entered, then 'null' is returned and the user is
informed that there are no files in the current
directory that contain that name
If the current directory has just one file that contains this name, this
file is automatically returned
If the current directory has multiple files that contain this name, the
user is asked which file that they want to use
@return File The name of the text file that will be used
@see gradeTextFile()
*/
private static File getFileByName() {
File[] fileList = getTextFiles( new String[]{ "solution", "grade" } );
if( fileList.length == 0 ) {
SOPln("\nNo files found that can be graded.");
return null;
}
if( fileList.length == 1 ) {
return fileList[0];
}
boolean fileFound = false;
do {
printFileListByName( fileList );
SOPln("\nWhich file?");
String response = new Scanner(System.in).nextLine().toLowerCase();
//If enter number index
boolean invalidIndex = false;
if( isNumeric( response.trim().replaceAll("\\.", "" ) ) ) {
try {
response = response.trim().replaceAll("\\.", "" );
int indexPos = Integer.parseInt( response ) - 1;
return fileList[ indexPos ];
} catch( NumberFormatException e ) {
//treat as a name entered, skip exception
} catch( IndexOutOfBoundsException e ) {
SOPln("\nInvalid index. Please enter a number between 1 and " + fileList.length );
invalidIndex = true;
}
}
if( !response.contains(".txt") ) response += ".txt";
//if enter the name of the file ^ v
if( !invalidIndex )
for( int i = 0; i < fileList.length; i++ ) {
if( response.equals( fileList[i].getName().toLowerCase() ) )
return fileList[i];
}
if( !invalidIndex ) SOPln("\nFile not found. Try again.");
} while( !fileFound ); //always true if reaches this point
return null; //should not be reached
}
/**
Print the list of text file names in the list
@param fileList The list of text file names to print
@see getSolutionFile()
@see printFileListByName( File[] fileList )
*/
public static void printFileListByName( ArrayList<File> fileList ) {
int size = fileList.size();
SOPln();
for( int i = 0; i < size; i++ ) {
SOPln( (i+1) + ". " + fileList.get(i).getName() );
}
}
/**
Print the list of text file names in the array
@param fileList The array of text file names to print
@see getFileByName()
@see printFileListByName( ArrayList<File> fileList )
*/
public static void printFileListByName( File[] fileList ) {
SOPln();
for( int i = 0; i < fileList.length; i++ )
SOPln( (i+1) + ". " + fileList[i].getName() );
}
/**
Grades the student file by comparing it to the solution file.
This method gathers the files needed and runs the gradeTextFile(...) function
@see getFileByName()
@see getSolutionFile()
@see gradeTextFile( File studentFile, File solutionFile )
*/
public static void gradeTextFile() {
File studentFile = getFileByName();
File solutionFile = getSolutionFile();
gradeTextFile( studentFile, solutionFile );
}
/**
Grades the student file by comparing it to the solution file.
A response text file is generated that shows the incorrect answers, the student response,
the automated response, and the total score. This file's name follows the structure of
"NAME_ASSIGNMENT_GRADE.txt"
This method can be optimized by changing the properties of the GRADER_SETTINGS_FILE_NAME file,
by direct editing or through using the menu options
These settings allow for the grader to grade using absolute or partial credit, adding or
withholding automated responses, setting extra credit, excluding problems from being graded,
accepting alternate answers for partial or full credit, accepting solutions within a given
range, and using scaled partial credit for solutions landing within a given range
@param studentFile The student text file to be graded. The name of the file should follow the format of "NAME_ASSIGNMENT.txt"
@param solutionFile The solution text file that contains the correct answers. The name of the file should follow the
format of "Solutions_ASSIGNMENT.txt"
@see gradeAllTextFiles()
@see checkIfFilesAreCompatible( String studentFileName, String solutionFileName )
*/
private static void gradeTextFile( File studentFile, File solutionFile ) {
String studentFileName = studentFile.getName();
String studentName = studentFileName.substring( 0, studentFileName.indexOf("_") );
String solutionFileName = solutionFile.getName();
boolean namesMatch = checkIfFilesAreCompatible( studentFileName, solutionFileName );
if( !namesMatch ) {
SOPln("The files " + studentFileName + " and " + solutionFileName + " are not compatible for grading.\n" +
"Each file must be a .txt file and have the same assignment name. The solution file must\n" +
"begin with the word \"solution\" (caps do not matter). The student file must\n" +
"begin with their name.");
return;
}
double totalPoints, maxPoints;
totalPoints = maxPoints = 0.0;
Scanner studentScanner = getScanner( studentFile );
Scanner solutionScanner = getScanner( solutionFile );
/*Student file format examples: Solution file format examples:
1. 23 1. 23 & 23.0 & 92;Multiplied by 2 instead of dividing;0.0 & 17;Subtracted instead of added;0.5
2. Rectangle 2. Rectangle & Rect & Parallelogram & Paralelogram & Square; Can't be a square because side C and side B are longer than A and D; 0.5
3. 4/5 3. 4/5 & 0.8
4. (4, 5) 4. (4, 5) & (4,5) & 4, 5 & 4,5 & x = 4, y = 5 & 5, 4 | (5, 4) ; Values are switched! ; 0.5 & (-4, -5) | -4, -5 ; x and y must be positive in order to make the left side equal to zero ; 0.5
5. 342.56 5. 342.57 ; Range 1.0 & 342.57 ; Range 1.0 to 5.0 ; 0.5 ; Didn't multiply by acceleration?
*/
String resultsFileText = "";
String printName = capFirstLetter( studentName ) + ":";
resultsFileText += printName + "\n";
SOPln( printName );
int lineNumber = 1;
ArrayList<String> responseLines = new ArrayList<String>();
while( studentScanner.hasNextLine() && solutionScanner.hasNextLine() ) {
//Get relevant text
String studentLine = studentScanner.nextLine();
String originalLine = studentLine;
String solutionLine = solutionScanner.nextLine();
studentLine = studentLine.substring( studentLine.indexOf(".") + 1, studentLine.length() ).trim();
solutionLine = solutionLine.substring( solutionLine.indexOf(".") + 1, solutionLine.length() ).trim();
String problemNumber = originalLine.substring( 0, originalLine.indexOf(".") );
//Handle if total student problems and total solution problems differ
if( studentLine.isEmpty() ) {
maxPoints += 1.0;
continue;
}
if( solutionLine.isEmpty() ) {
SOPln("\nError! The number of solutions for the assignment " + solutionFileName + " is less than the\n" +
"number of answers given in the student file " + studentFileName + ".\n" +
"\nPlease fix this before grading this file.\n");
return;
}
//Formatting
studentLine = studentLine.toLowerCase();
studentLine = checkAndConvertToDecimal( studentLine );
solutionLine = solutionLine.toLowerCase();
if( !spacesMatter ) studentLine = removeWhitespace( studentLine );
//Save info for response file
String responseLine = problemNumber + ". ";
String autoFeedback = "";
String[] solutionParts = solutionLine.split("&");
String firstSolution = findFirstCorrectSolution( solutionParts );
boolean correctAnswer = false;
boolean hitCorrectOrPartialAnswer = false;
for( int i = 0; i < solutionParts.length; i++ ) {
String[] solutionSpecs = solutionParts[i].trim().split(";");
//No specs
if( solutionSpecs.length == 1 && studentLine.equals( solutionParts[i] ) ) {
correctAnswer = true;
break;
}
//Handle specs
double minRange = 0.0;
double maxRange = 0.0;
double partialCreditRatio = 1.0;
double studentLineValue = 0.0;
int studentLineValueInt = 0;
double solutionLineValue = 0.0;
int solutionLineValueInt = 0;
boolean doubleFail = false;
boolean intFail = false;
boolean doubleFailSolution = false;
boolean intFailSolution = false;
String solutionValue = "";
//Parse student answer values
try {
studentLineValue = Double.parseDouble( solutionSpecs[0] );
} catch( NumberFormatException e ) {
doubleFail = true;
}
if( doubleFail ) {
try {
studentLineValueInt = Integer.parseInt( solutionSpecs[0] );
} catch( NumberFormatException e ) {
intFail = true;
}
}
//Run through each spec and set settings
for( int j = 0; j < solutionSpecs.length; j++ ) {
doubleFailSolution = intFailSolution = false;
solutionSpecs[j] = solutionSpecs[j].trim();
//Check for range tag and set range
if( solutionSpecs[j].contains("range") ) {
String[] rangeSpecs = solutionSpecs[j].split(" ");
if( solutionSpecs[j].contains("to") ) {
//Set min range
try {
minRange = Double.parseDouble( rangeSpecs[1] );
} catch( NumberFormatException e ) {
try {
minRange = Integer.parseInt( rangeSpecs[1] );
} catch( NumberFormatException f ) {
SOPln("\nThis line should not be reached! Search tag: @471");
return;
}
}
//Set max range
try {
maxRange = Double.parseDouble( rangeSpecs[3] );
} catch( NumberFormatException e ) {
try {
maxRange = Integer.parseInt( rangeSpecs[3] );
} catch( NumberFormatException f ) {
SOPln("\nThis line should not be reached! Search tag: @483");
return;
}
}
} else { //no min range
try {
maxRange = Double.parseDouble( rangeSpecs[1] );
} catch( NumberFormatException e ) {
try {
maxRange = Integer.parseInt( rangeSpecs[1] );
} catch( NumberFormatException f ) {
SOPln("\nThis line should not be reached! Search tag: @491");
return;
}
}
}
continue;
}
//Check for solution value and partial credit ratio
if( j == 0 || isNumeric( solutionSpecs[j] ) ) {
//Parse solution values
double tempDouble = 0.0;
int tempInt = 0;
try {
if( j == 0 ) solutionLineValue = Double.parseDouble( solutionSpecs[j] );
else tempDouble = Double.parseDouble( solutionSpecs[j] );
} catch( NumberFormatException e ) {
doubleFailSolution = true;
try {
if( j == 0 ) solutionLineValueInt = Integer.parseInt( solutionSpecs[j] );
else tempInt = Integer.parseInt( solutionSpecs[j] );
} catch( NumberFormatException f ) {
intFailSolution = true;
}
}
boolean isPartialCreditValue = false;
//Double value
if( !doubleFailSolution && j != 0 )
isPartialCreditValue = tempDouble < 1.0 && tempDouble > -1.0;
//Int value
else if( !intFailSolution && j != 0 )
isPartialCreditValue = tempInt < 1 && tempInt > -1;
if( isPartialCreditValue )
partialCreditRatio = Double.parseDouble( solutionSpecs[j] );
else
solutionValue = solutionSpecs[j]; //This is a String
continue;
}
//Check for automated response
if( !isNumeric( solutionSpecs[j] ) ) {
autoFeedback = solutionSpecs[j];
continue;
} else {
SOPln("Error! Formatting issue within solution file '" + solutionFileName + "'.\n" +
"See line #" + lineNumber + ". One or more solution tags does not follow the format of\n" +
"Problem#. Solution Value ; Range NUMBER ; Range NUMBER to NUMBER ; PARTIAL CREDIT RATIO\n" +
"where the range and partial credit tags are optional. For solution tags that do not have\n" +
"the range or partial credit tags, there should be no semicolons present. In that case, the\n" +
"solution should look like the following:\n" +
"Problem#. Solution Value\n\nPlease edit this line and then run this program again.\n");
return;
}
}//end for loop for looking through specs seperated by semicolons
/*Check correctness based on solution tags*/
//String response
if( intFail || intFailSolution ) {
if( studentLine.equals( solutionValue ) ) {
totalPoints += defaultPointValue * partialCreditRatio;
if( partialCreditRatio < 1.0 )
responseLine += "XXX Partial Credit: " + (defaultPointValue * partialCreditRatio) + "/" + defaultPointValue + ". Student answer: " + studentLine + ", Solution: " + firstSolution.trim()/*(!doubleFailSolution ? solutionLineValue : solutionLineValueInt)*/ + ". " + autoFeedback;
else
responseLine += "Correct";
hitCorrectOrPartialAnswer = true;
break;
//Check to see if solutions are correct, but are just rearranged in different orders.
//Note that this requires the 'listOrderMatters' setting to be set to false
} else if( !listOrderMatters && studentLine.contains(",") ) {
String[] studentList = studentLine.replaceAll("\\s+","").split(",");
String[] solutionList = solutionValue.replaceAll("\\s+","").split(",");
if( studentList.length == solutionList.length && studentList.length != 0 && solutionList.length != 0 ) {
boolean passed = true;
for( int j = 0; j < solutionList.length; j++ ) {
if( !solutionLine.contains( studentList[j] ) )
passed = false;
}
//The lists are equal, but possibly rearranged differently
if( passed ) {
totalPoints += defaultPointValue * partialCreditRatio;
if( partialCreditRatio < 1.0 )
responseLine += "XXX Partial Credit: " + (defaultPointValue * partialCreditRatio) + "/" + defaultPointValue + ". Student answer: " + studentLine + ", Solution: " + firstSolution.trim()/*(!doubleFailSolution ? solutionLineValue : solutionLineValueInt)*/ + ". " + autoFeedback;
else
responseLine += "Correct";
hitCorrectOrPartialAnswer = true;
break;
}
}
}
} else { //For int or double values
if( Math.abs( studentLineValue - solutionLineValue ) <= maxRange && Math.abs( studentLineValue - solutionLineValue ) >= minRange ) {
totalPoints += defaultPointValue * partialCreditRatio;
if( partialCreditRatio < 1.0 )
responseLine += "XXX Partial Credit: " + (defaultPointValue * partialCreditRatio) + "/" + defaultPointValue + ". Student answer: " + studentLine + ", Solution: " + firstSolution.trim()/*(!doubleFailSolution ? solutionLineValue : solutionLineValueInt)*/ + ". " + autoFeedback;
else
responseLine += "Correct";
hitCorrectOrPartialAnswer = true;
break;
}
}
autoFeedback = "";
} //end for loop checking all solutions
//Update points
if( correctAnswer ) {
totalPoints += 1.0;
responseLine += "Correct";
} else if( !hitCorrectOrPartialAnswer )
responseLine += "XXX Incorrect: 0/" + defaultPointValue + ". Student answer: " + studentLine + ", Solution: " + firstSolution.trim();
maxPoints += defaultPointValue;
lineNumber++;
resultsFileText += responseLine + "\n";
SOPln( responseLine );
} //end while going through files
String score = "\nScore: " + totalPoints + " / " + maxPoints;
resultsFileText += score;
SOPln( score );
SOPln( "------------------------------------" );
//Write results file
if( createResultsFile ) {
String newFileName = studentFileName.substring( 0, studentFileName.indexOf(".") ) + "_Grade.txt";
createTextFile( newFileName );
writeToFile( newFileName, resultsFileText );
}
}
/**
Finds and returns the correct solution value, if it exists, for these
specs, which are delimited by semicolons
@param solutionSpecs The specs for this answer
@return String If the answer is correct, returns the correct answer. If it
is an answer that is incorrect, or would receive partial
credit, returns the empty string ""
*/
private static String findFirstCorrectSolution( String[] solutionParts ) {
for( int partCount = 0; partCount < solutionParts.length; partCount++ ) {
String[] parts = solutionParts[partCount].split(";");
boolean skip = false;
for( int rep = 1; rep < parts.length; rep++ ) {
if( isNumeric( parts[rep].trim() ) ) {
skip = true;
break;
}
}
if( skip ) continue;
return parts[0].trim();
}
return "";
}
/**
Checks if the student file and the solution file are compatible.
Checks for the following:
1) The student file name should be in the format NAME_ASSIGNMENT.txt
2) The solution file name should be in the format Solution_ASSIGNMENT.txt
3) Both files end in the same assignment name, followed by .txt
@param studentFileName The name of the student file. Includes the .txt extension
@param solutionFileName The name of the solution file. Includes the .txt extension
@return boolean True if the files are compatible, false otherwise
@see gradeTextFile( File studentFile, File solutionFile )
*/
private static boolean checkIfFilesAreCompatible( String studentFileName, String solutionFileName ) {
//1) The student file name should be in the format NAME_ASSIGNMENT.txt
String[] tokens = studentFileName.split("_|-");
if( tokens.length < 2 ) return false;
if( tokens[0].length() == 0 ) return false;
String assignmentName = tokens[1].substring(0, tokens[1].length() - 4);
if( assignmentName.length() == 0 ) return false;
//2) The solution file name should be in the format Solution_ASSIGNMENT.txt
String[] solutionTokens = solutionFileName.split("_|-");
if( solutionTokens.length < 2 ) return false;
if( solutionTokens[0].length() == 0 ) return false;
if( !solutionTokens[0].toLowerCase().contains("solution") ) return false;
String solutionAssignmentName = solutionTokens[1].substring(0, solutionTokens[1].length() - 4);
if( solutionAssignmentName.length() == 0 ) return false;
//3) Both files end in the same assignment name, followed by .txt
if( !assignmentName.toLowerCase().equals( solutionAssignmentName.toLowerCase() ) ) return false;
return true;
}
/**
Change the settings for grading files
*/
public static void changeGradingOptions() {
String response = "";
Scanner fileScanner = null;
File graderSettingsFile = new File( GRADER_SETTINGS_FILE_NAME );
String graderSettingsText = getTextFromFile( graderSettingsFile );
int changeOptionNumber = 0;
boolean runAgain = true;
do {
SOPln("\nWhich setting would you like to change? Enter the number to switch the setting\n");
printSettings( graderSettingsText );
String option = scanner.nextLine();
if( option.toLowerCase().contains("q") ) {
runAgain = false;
continue;
}
try {
changeOptionNumber = Integer.parseInt( option ) - 1;
} catch( NumberFormatException e ) {
e.printStackTrace();
}
if( changeOptionNumber >= 0 )
graderSettingsText = changeSettings( graderSettingsText, changeOptionNumber );
pause();
} while( runAgain );
//Overwrite file with new settings. Will have 0, 1 or more changes
writeToFile( graderSettingsFile.getPath(), graderSettingsText );
}
/**
Change the settings by flipping the boolean on the option selected
@param graderSettingsText The text to change one of the options
@param changeOptionNumber The line of the settings file to be changed
@return String The text of file, now with an option that has been changed
*/
public static String changeSettings( String graderSettingsText, int changeOptionNumber ) {
String[] lines = graderSettingsText.split("\r?\n|\r");
if( lines.length <= changeOptionNumber ) {
SOPln("\nInvalid option number. Please enter a number between 1 and " + lines.length + ", inclusive.\n");
return graderSettingsText;
}
String[] parts = lines[ changeOptionNumber ].split("\\?");
if( parts[1].toLowerCase().trim().equals("true") )
lines[ changeOptionNumber ] = parts[0] + "? false";
else if( parts[1].toLowerCase().trim().equals("false") )
lines[ changeOptionNumber ] = parts[0] + "? true";
else {
SOPln("\nWhat will the new value be?\n");
lines[ changeOptionNumber ] = parts[0] + "? " + scanner.nextInt() + scanner.nextLine();
}
String newSettings = "";
for( int i = 0; i < lines.length; i++ ) {
if( i != lines.length - 1 ) newSettings += lines[i] + "\n";
else newSettings += lines[i];
}
return newSettings;
}
/**
Print the settings String with formatting
@param graderSettingsText The text of the GraderSettings.txt file
*/
public static void printSettings( String graderSettingsText ) {
String[] lines = graderSettingsText.split("\r?\n|\r");
for( int i = 0; i < lines.length; i++ ) {
SOPln( (i+1) + ". " + lines[i] );
}
SOPln("\nEnter Q to quit\n");
}
/**
Creates a solution file by I/O with the user
Solution files must begin with the word solution and include a title for the assignment/test.
Solutions should be separated by &'s. Any solution that is worth partial credit, or solutions with
set ranges, or solutions with automated responses should be separated by semicolons, where the
solution appears first. The order for the other settings do not matter. Spaces and capitalization
also does not matter.
*/
public static void createSolutionFile() {
SOPln("\nWhat is the name of this assignment/quiz/test/etc ?\n");
String fileName = scanner.nextLine();
if( !fileName.toLowerCase().contains("solution") ) fileName = "Solutions_" + fileName;
if( !fileName.contains(".txt") ) fileName += ".txt";
if( !createTextFile( fileName ) ) return; //Create new text file with name. If something goes wrong, return
SOPln("\nWhat numbers are part of the assignment?\n\tEg. Enter individual numbers, like 1,2,5,10,12,13,15,\n" +
"\tor use ranges, such as 1,3,5,10-15,17,30-32, or you may specify odd or evens,\n" +
"\tsuch as 1-20 evens, or a combination of these:\n" +
"\t1,4,5,8-20 evens,23,26,27,29,41-45 odds. You can also specify subsections,\n" +
"\tsuch as the following: 1,4,5a-d,7,8-10,14-18 evens,20b,21c,23c-g,25-31 odds,33-35");
String problems = scanner.nextLine();
String[] problemList = createProblemList( problems );
String problemStringImmutabilityIssuesSoUseThis = toSingleString( problemList );
if( problemList.length == 0 ) return;
String[] numbersAndSolutions = addSolutions( problemList );
//Create solution file and template file for students
writeToFile( fileName, toSingleString( numbersAndSolutions ) );
String templateFileName = "Temp_" + fileName.replaceAll("Solutions_","");
writeToFile( templateFileName, problemStringImmutabilityIssuesSoUseThis );
//Add macro substitutions
addMacroSubstitutions( fileName );
SOPln("\nSuccessfully created \"" + fileName + "\"");
SOPln("\nSuccessfully created \"" + templateFileName + "\"");
}
/**
Reformats solution files to add alternate solutions
*/
public static void reformatSolutionFile() {
File solutionFile = getSolutionFile();
addMacroSubstitutions( solutionFile.getName() );
}
/**
Adds any substitutions to a solution file.
These substitutions, or macros, are for the automatic expansion of
common alternative answers. This method also breaks apart lists of
alternate answers that share the same specs by breaking these into
separate parts, as designated by the "|" punctuation.
Algebraic substitutions are included here, a topic of which can be
continually improved upon
@param fileName The solution file to change
*/
private static void addMacroSubstitutions( String fileName ) {
Scanner sc = getScanner( fileName );
if( sc == null ) return;
//Go through solution file
String newLine = "";
while( sc.hasNextLine() ) {
String line = sc.nextLine().toLowerCase();
String[] parts = line.split("&");
int singleSpecBuffer = 0;
//Go through parts (alternate solutions)
for( int partNum = 0; partNum < parts.length; partNum++ ) {
String[] pieces = parts[ partNum ].split(";");
String solution = pieces[0];
String pieceLine = "";
//Get piece information
for( int pieceNum = 1; pieceNum < pieces.length; pieceNum++ ) {
pieceLine += ";" + pieces[ pieceNum ];
}
//I wrote this in the airport having not slept for like 48 hours. I don't even know what's going on
if( pieces.length > 0 && pieces.length != 1 && partNum == parts.length - 1 )
pieceLine += " "; //Need additional spaces for alternate solutions using pipes
else if( pieces.length > 0 && pieces.length == 1 && partNum == parts.length - 1 )
singleSpecBuffer = 1;
if( solution.contains("|") ) {
String[] alts = solution.split("\\|");
//Go through alternates with the same piece information
for( int altNum = 0; altNum < alts.length; altNum++ ) {
//Apply algebraic solutions, if setting is turned on
ArrayList<String> substitutions = null;
if( includeAlgebraicAlternates )
substitutions = applyAlgebraicMacros( alts[ altNum ] );
if( substitutions != null ) {
int totalSubs = substitutions.size();
for( int rep = 0; rep < totalSubs; rep++ ) {
newLine += substitutions.get( rep ) + pieceLine + "&";
}
} else //No algebraic substitutions necessary
newLine += alts[ altNum ] + pieceLine + "&";
}
//No lists of alternate solutions with the same piece information
} else {
//Apply algebraic solutions, if setting is turned on
ArrayList<String> substitutions = null;
if( includeAlgebraicAlternates )
substitutions = applyAlgebraicMacros( solution );
if( substitutions != null ) {
int totalSubs = substitutions.size();
for( int rep = 0; rep < totalSubs; rep++ ) {
newLine += substitutions.get( rep ) + pieceLine + "&";
}
} else //No algebraic substitutions necessary
newLine += solution + pieceLine + "&";
}
} //end parts loop
newLine = newLine.substring( 0, newLine.length() - 2 + singleSpecBuffer ); //remove last space and &
newLine += "\n";
} //end while loop for lines of text file
newLine = newLine.substring( 0, newLine.length() - 1 ); //remove last \n
writeToFile( fileName, newLine );
}
/**
Apply a series of algebraic substitutions to the given solution.
These algebraic substitutions are appended to the list of given solutions
or alternate solutions, or partial credit solutions, along with all of the
attached specs to that solution, with the idea being that these alternate
solutions are equivalent in form
Here are some examples:
1. Unary Algebraic Equivalence
a. 1x = x
b. x^1 = x
2. Decimal Floating Zero Equivalence
a. 8.0 = 8
b. 014 = 14
3. Associative Property Equivalence
a. xy = yx
b. x*y = xy
c. x*y = y*x
4. Communicative Property Equivalence
a. x + y = y + x
b. x - y = - y + x
5. Parenthetical Equivalence
a. x(y) = x * y
b. x^y = x^(y)