Home COMSC-171 <- Prev Next ->

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