Grade Calculation Script
Purpose
- calculate total and adjusted scores from a text file with scores in columns
- number, position, and width of input file columns may vary
- example input file
NAME SID4 Q1 Q2 Q3 Q4 E1 Q5 Q6 Q7 Q8 E2 Q9 QA QB QC P1
Doe, Jane 1234 8 9 7 10 42 9 10 7 9 45 8 9 9 40
- example output file
NAME SID4 Q1 Q2 Q3 Q4 E1 Q5 Q6 Q7 Q8 E2 Q9 QA QB QC P1 TOT ADJ
Doe, Jane 1234 8 9 7 10 42 9 10 7 9 45 8 9 9 40 222 192
Code
while read Line ; do
[[ $Line == '' || $Line == \#* ]] && echo "$Line" && continue
echo "$Line TOT ADJ"
Length=${#Line}
First=(0)
for (( Ix=1 ; Ix < ${#Line} ; ++Ix )) ; do
[[ ${Line:$Ix:1} != ' ' && ${Line:$Ix-1:1} == ' ' ]] && First+=($Ix) ; done
for Col in ${!First[@]} ; do
(( $Col == ${#First[@]} - 1 )) && End=${#Line} || End=${First[$Col+1]}
Head+=("${Line:${First[$Col]}:$(( $End-${First[$Col]} ))}") ; done
break ; done
while read Line ; do
[[ $Line == '' || $Line == \#* ]] && echo "$Line" && continue
Data=()
for Col in ${!First[@]} ; do
(( $Col == ${#First[@]} - 1 )) && End=${#Line} || End=${First[$Col+1]}
Data+=("${Line:${First[$Col]}:$(( $End-${First[$Col]} ))}") ; done
Quiz=() ; ExPr=()
for Field in ${!Head[@]} ; do
[[ ${Head[Field]} == Q[0-9A-F]* ]] && Quiz+=($(( ${Data[Field]}+0 )))
[[ ${Head[Field]} == [EP][0-9]* ]] && ExPr+=($(( ${Data[Field]}+0 ))) ; done
Quiz=($(printf "%d\n" ${Quiz[@]} | sort -n))
LowQuiz=$(( ${Quiz[0]}+${Quiz[1]}+${Quiz[2]}+${Quiz[3]}+${Quiz[4]} ))
ExPr=($(printf "%s\n" "${ExPr[@]}" | sort -n))
LowExPr=${ExPr[0]}
(( $LowQuiz < $LowExPr )) && Drop=$LowQuiz || Drop=$LowExPr
Tot=0
for Qnum in ${!Quiz[@]} ; do
(( Tot+=${Quiz[Qnum]} )) ; done
for Enum in ${!ExPr[@]} ; do
(( Tot+=${ExPr[Enum]} )) ; done
Adj=$(( $Tot - $Drop ))
printf "%-${Length}s%4s%4s\n" "$Line" "$Tot" "$Adj" ; done
Explanation
- The first section reads the header line and finds the character positions of the start of each heading.
while read Line ; do
- read header line and any preceding blank or comment (begin with
#) lines
[[ $Line == '' || $Line == \#* ]] && echo "$Line" && continue
- if line is blank or begins with
# then print it and continue to next iteration
echo "$Line TOT ADJ"
- print header line with 2 more fields for total and adjusted total
Length=${#Line}
- save length of header line for subsequent output
First=(0)
- array for positions of the first char in each field, 1st element 0
for (( Ix=1 ; Ix < ${#Line} ; ++Ix )) ; do
- loop over each character in header line (Ix is index)
[[ ${Line:$Ix:1} != ' ' && ${Line:$Ix-1:1} == ' ' ]] && First+=($Ix) ; done
- if char at Ix is not space and char at previous Ix is space then append Ix to First array
for Col in ${!First[@]} ; do
- loop over indexes in First
(( $Col == ${#First[@]} - 1 )) && End=${#Line} || End=${First[$Col+1]}
- if last columm then end of column is last char of Line else end of column is first char of next column
Head+=("${Line:${First[$Col]}:$(( $End-${First[$Col]} ))}") ; done
- append appropriate substring (start index, count) of Line to array of headings
break ; done
- header processing finished, exit loop
- The second section reads the data lines, divides them into fields defined by the character positions in the header, and calculates totals of fields which have headings matching quizzes, exams, or programs.
while read Line ; do
- read data lines and any blank or comment lines
[[ $Line == '' || $Line == \#* ]] && echo "$Line" && continue
- if line is blank or begins with
# then print it and continue to next iteration
Data=()
- initialize array of scores for each iteration
for Col in ${!First[@]} ; do
- loop over indexes in First
(( $Col == ${#First[@]} - 1 )) && End=${#Line} || End=${First[$Col+1]}
- if last columm then end of column is last char of Line else end of column is first char of next column
Data+=("${Line:${First[$Col]}:$(( $End-${First[$Col]} ))}") ; done
- append appropriate substring (start index, count) of Line to array of scores
Quiz=() ; ExPr=()
- initialize arrays of quiz scores and exam/program scores for each iteration
for Field in ${!Head[@]} ; do
- loop over indexes in header array
[[ ${Head[Field]} == Q[0-9A-F]* ]] && Quiz+=($(( ${Data[Field]}+0 )))
- if heading matches quiz then append corresponding data to quiz array (add 0 to make it numeric)
[[ ${Head[Field]} == [EP][0-9]* ]] && ExPr+=($(( ${Data[Field]}+0 ))) ; done
- if heading matches exam/program then append corresponding data to exam/program array
Quiz=($(printf "%d\n" ${Quiz[@]} | sort -n))
- output values separated with newlines, pipe to
sort, assign back to quiz array
LowQuiz=$(( ${Quiz[0]}+${Quiz[1]}+${Quiz[2]}+${Quiz[3]}+${Quiz[4]} ))
- add 5 lowest quiz scores (this is simpler than a loop)
ExPr=($(printf "%s\n" "${ExPr[@]}" | sort -n))
- output values separated with newlines, pipe to
sort, assign back to exam/program array
LowExPr=${ExPr[0]}
- lowest exam/program score
(( $LowQuiz < $LowExPr )) && Drop=$LowQuiz || Drop=$LowExPr
- set score(s) to drop to smaller of quizzes or exam/program
Tot=0
- initialize total score for each iteration
for Qnum in ${!Quiz[@]} ; do
- loop over indexes in quiz score array
(( Tot+=${Quiz[Qnum]} )) ; done
- add current score to total
for Enum in ${!ExPr[@]} ; do
- loop over indexes in exam/program score array
(( Tot+=${ExPr[Enum]} )) ; done
- add current score to total
Adj=$(( $Tot - $Drop ))
- subtract dropped score(s) from total
printf "%-${Length}s%4s%4s\n" "$Line" "$Tot" "$Adj" ; done
- print Line in field matching header, then total and adjusted fields
Notes
- programming style
&& and || are used instead of simple if statements to reduce the number of lines
- simple related commands are combined on single lines to reduce the number of lines
- blank lines and spaces are added for readability
- performance
- the only external program this program calls is
sort
- re-implementing
sort in bash would not be worth the trouble
- finding the 5 lowest quiz scores without sorting would not be worth the trouble
- limitations
- this program uses
bash extensions and will not run on simpler shells
- this program won't handle improperly formatted input lines