Exam Points Table – Creating a Custom Points Table for Exams in LaTeX

counterscross-referencingexamexamdesignexpl3

I'm trying to create a custom points table for exams. Currently, the instructors in our department write these exams in MS Word, so to ease the growing pains, I'd like to emulate the old design as closely as possible.

Sample exam grade table

I am aware that the exam package has a built in grade table, but it is a different format. Furthermore, for questions with subparts (e.g. 1a-1h), there's no way to directly access the point values of each (the exam package has \pointsofquestion{#}, but it only refers to the top level).

Ideally, I'd like to be able to automatically compute the Multiple Choice and Free Response point totals and populate the "Points Possible" columns of the table. Automatically generating the table seems a little too far fetched (number of rows may vary), but I'm not opposed to a solution for that too.

Edit 1:

As mentioned in the comments, I've been hacking up the code posted in the referenced answer. At the moment, I'm able to grab all the labels for the question parts along with the point values to create the code for the table. Below is a MWE of getting the labels and point values. I've cut out the specific code that builds the table as I can figure that out that syntax fairly easily now.

\documentclass[addpoints]{exam}

\usepackage{xparse,xpatch}

% redefine \question command to be \myquest
\appto\questions{\let\examquest\question\let\question\myquest}
% redefine \part command to be \mypart
\appto\parts{\let\exampart\part\let\part\mypart}


\ExplSyntaxOn
\tl_new:N \g_grade_list_tl % this is a grading list

\int_new:N \g_mcscore_int% this will be the multiple choice score
\int_new:N \g_frscore_int% this will be the free response score
\int_new:N \g_exscore_int% this will be the total exam score
%% Add question parts to grading list
\NewDocumentCommand\mypart{o}{
  \IfNoValueTF{#1}{\exampart}{
    % don't do anything special inside solutions
    \if@insolution\exampart[#1]
    \else\exampart[#1]
      \int_gadd:Nn \g_frscore_int {#1}
      \tl_gput_right:Nx \g_grade_list_tl {\arabic{question}\alph{partno},}
      \tl_gput_right:No \g_grade_list_tl {#1,}
    \fi
  }
}

\NewDocumentCommand\prtGradeList{}{
\tl_use:N \g_grade_list_tl}
\ExplSyntaxOff

\begin{document}

\begin{questions}
  \question
    \begin{parts}
      \part[1]
      \part[2]
    \end{parts}
  \question
    \begin{parts}
        \part[4]
        \part[2]
    \end{parts}
  \question
    \begin{parts}
        \part[2]
        \part[4]
        \part[4]
        \part[1]
      \end{parts}
  \question[5] 
\end{questions}

\prtGradeList{}

\end{document}

What I have been able to make is a document with several questions followed by a grading table and then display the values and points under the table. The values under the table are just for testing purposes and won't be in the final product. Currently the vertical line on the RHS of the table is missing, but that's only because I haven't finished each row.
Sample exam grade table v2

Thus, what I think my next step is now is to find the length of the list of labels and values, divide it in "half" (if the number of questions/parts is odd, I'd like to make the RHS longer).

Finally, the icing on the cake would be if I could place this table on the first page of the document. I haven't figured out how to do that yet as I'm not sure if I should save the table to an external file or something else.

Edit 2:

After reading the expl3 documentation and experimenting, I'm trying to build the table using sequences:

\documentclass[addpoints]{exam}

\usepackage{xparse,xpatch,multirow}
\usepackage[table,xcdraw]{xcolor}
\definecolor{rowGray}{HTML}{EFEFEF}
\def\scantronPt{1} %% Scantron point
\def\numGradeCols{2} 

% redefine \question command to be \myquest
\appto\questions{\let\examquest\question\let\question\myquest}
% redefine \part command to be \mypart
\appto\parts{\let\exampart\part\let\part\mypart}

\makeatletter
\ExplSyntaxOn
\tl_new:N \g_grade_table_tl% this will; become the new grade table
\seq_new:N \g_grade_seq % this is a grading sequence

\int_new:N \g_mcscore_int% this will be the multiple choice score
\int_new:N \g_frscore_int% this will be the free response score
\int_new:N \g_exscore_int% this will be the total exam score

%% Add question parts to grading sequence
\NewDocumentCommand\mypart{o}{
  \IfNoValueTF{#1}{\exampart}{
    \if@insolution\exampart[#1]
    \else\exampart[#1]
      \int_gadd:Nn \g_frscore_int {#1}
      \seq_gput_right:Nx \g_grade_seq {\arabic{question}\alph{partno}}
      \seq_gput_right:No \g_grade_seq {#1}
    \fi
  }
}

%% Add question to grading sequence
\NewDocumentCommand\myquest{o}{
  \IfNoValueTF{#1}{\examquest}{
    \if@insolution\examquest[#1]
    \else\examquest[#1]
      \int_gadd:Nn \g_frscore_int {#1}
      \seq_gput_right:Nx \g_grade_seq {\arabic{question}}
      \seq_gput_right:No \g_grade_seq {#1}
    \fi
  }
}

\NewDocumentCommand\GradeTable{}{% the new grade table
  %\BuildGradeTable{}
  \seq_gput_right:Nn \g_grade_seq {Scantron}
  \seq_gput_right:Nx \g_grade_seq {\scantronPt}
  \seq_new:N \g_gradeLeft_seq

  %% Macro
  \def\seqLen{\seq_count:N \g_grade_seq} 
  \def\seqLeftLen{\seq_count:N \g_gradeLeft_seq}

  %% Grab sequence original length
  \int_const:Nn \seqOrigLen \seqLen

  %% Create two integer variables
  \int_new:N \leftSideLen \int_new:N \rightSideLen
  %% Compute length of left and right columns
  \int_gset:Nn \leftSideLen {\int_eval:n {2*\int_div_truncate:nn \seqOrigLen {4}}} 
  \int_gset:Nn \rightSideLen {\int_eval:n {\seqOrigLen-\int_use:N \leftSideLen}} 

  %% Split sequence in two
  \int_do_until:nNnn {\seqLen} = {\int_use:N \rightSideLen} {
    \seq_gpop:NN \g_grade_seq \l_tmpa_tl
    \seq_gpush:Nx \g_gradeLeft_seq \l_tmpa_tl
  }
  \seq_reverse:N \g_gradeLeft_seq
  %% Displays sequences in terminal (debugging purposes)
  %\seq_show:N \g_gradeLeft_seq
  %\seq_show:N \g_grade_seq

  %% Build Table
  \int_do_until:nNnn {\seqLeftLen} = {0} {
    \tl_gput_right:Nn \g_grade_table_tl {\hline}
    \seq_gpop:NN \g_gradeLeft_seq \l_tmpa_tl
    \tl_gput_right:No \g_grade_table_tl {\l_tmpa_tl & }
    \seq_gpop:NN \g_gradeLeft_seq \l_tmpa_tl
    \tl_gput_right:No \g_grade_table_tl {\l_tmpa_tl &&}
    \seq_gpop:NN \g_grade_seq \l_tmpb_tl
    \tl_gput_right:No \g_grade_table_tl {\l_tmpb_tl &}
    \seq_gpop:NN \g_grade_seq \l_tmpb_tl
    \tl_gput_right:No \g_grade_table_tl {\l_tmpb_tl & \\}
  }
  \seq_if_empty:NF \g_grade_seq {
    \tl_gput_right:Nn \g_grade_table_tl {\hline \multicolumn{2}{r}{}& }
    \seq_gpop:NN \g_grade_seq \l_tmpa_tl
    \tl_gput_right:No \g_grade_table_tl {\l_tmpa_tl &}
    \seq_gpop:NN \g_grade_seq \l_tmpa_tl
    \tl_gput_right:No \g_grade_table_tl {\l_tmpa_tl & \\}
  }
  %\seq_show:N \g_gradeLeft_seq
  %\seq_show:N \g_grade_seq
  \tl_show:N \g_grade_table_tl
  \renewcommand{\arraystretch}{1.7}

  %\tl_gput_right:Nn \g_grade_table_tl {\hline Scantron&\scantronPt &&\\}
  \int_gadd:Nn \g_frscore_int {\scantronPt }
  \int_gadd:Nn \g_mcscore_int {\g_frscore_int}
  \tl_gclear:N \g_grade_table_tl
  \begin{center}
    \begin{tabular}{|*{6}{c|}} %% This syntax repeats column types
\multicolumn{6}{c}{\textit{\textbf{For~instructor~or~teaching~assistant~use~only.}}}\\[5pt]\hline
    \rowcolor{rowGray}
    \multicolumn{1}{|r|}{\textbf{Question}} & \multicolumn{1}{r|}{\textbf{Points~Possible}} & \multicolumn{1}{r|}{\textbf{Points~Earned}} & \textbf{Question} & \textbf{Points~Possible} & \textbf{Points~Earned}\\ \hline
    %\tl_use:N \g_grade_table_tl \hline
    \multicolumn{2}{r}{} & \multicolumn{2}{|r|}{\textbf{Multiple~Choice}} & \int_use:N \g_mcscore_int & \\ \cline{3-6} 
    \multicolumn{2}{l}{} & \multicolumn{2}{|r|}{\textbf{Free~Response}} & \int_use:N \g_frscore_int &\\ \cline{3-6} 
    \multicolumn{2}{l}{} & \multicolumn{2}{|r|}{\textit{\textbf{Exam~Total}}} & \int_use:N \g_exscore_int &\\ \cline{3-6} 
    \end{tabular}
  \end{center}
}
\ExplSyntaxOff
\makeatother

\begin{document}

\begin{questions}
\question
What if there were no air?
\begin{parts}
\part[1]
Describe the effect on the balloon industry.
\part[2]
Describe the effect on the aircraft industry.
\end{parts}
\question
\begin{parts}
  \part[4]
    Define the universe.
    Give three examples.
  \part[2]
    If the universe were to end, how would you know?
\end{parts}
\question
\begin{parts}
  \part[2]
  \part[4]
  \part[4]
  \part[1]
  \part[1]
\end{parts}
\question[5] 
%\question[1]
\end{questions}

\GradeTable{}

\end{document}

At this point, I can see that my \g_grade_table_tl gives me what I want in the .log file, but when I run this code through pdflatex, it gets stuck at the \GradeTable{} function.

Best Answer

As you want to put the scores for the question parts in two columns, and as the number of scores is variable, you will need to store the scores somewhere and then generate the whole table at the end. Building on my previous idea, I would put the scores into a LaTeX3 sequence, whilst keeping track of the total score and the number of scores as you go. As in my other post, I would then define a \GradeBook command to produce your custom table:

enter image description here

I am not sure how "multiple choice" and "free response" questions are coded when using the exam class, which is why I asked for a minimal working example :), so in the code below I have cheated and hard-code these marks using:

\def\multiplechoice{54}
\def\freeresponse{46}

Apart from this, everything is automatic. Here is the full code:

\documentclass[addpoints]{exam}

\usepackage[table]{xcolor}
\usepackage{xparse,xpatch}
% redefine \part command to be \mypart
\appto\parts{\let\exampart\part\let\part\mypart}

\def\multiplechoice{54}
\def\freeresponse{46}
\makeatletter
\ExplSyntaxOn
% this will become a sequence of the part numbers and scores
% like: 1a&10&, 1b&8&, 1c&9&, 2a&6, 2b&8&, 3&12&, 4&14&, ...
\seq_new:N \g_part_scores_seq
\tl_new:N \g_grade_table_tl

\int_new:N \g_total_score_int% this will be the exam score
\int_new:N \g_number_of_scores_int
\NewDocumentCommand\mypart{o}{
  \IfNoValueTF{#1}{\exampart}{
    % don't do anything special inside solutions
    \if@insolution\exampart[#1]
    \else\exampart[#1]
      % store both the part number and score in \g_part_scores_seq
      % together with their column separators for the tabular env
      \tl_set:Nx \l_tmpa_tl { \arabic{question}\alph{partno} }
      \tl_put_right:Nn \l_tmpa_tl {&}
      \tl_put_right:No \l_tmpa_tl {#1}
      \tl_put_right:Nn \l_tmpa_tl {&}
      \seq_gput_right:No \g_part_scores_seq \l_tmpa_tl
      % increment the running total and number of scores
      \int_gadd:Nn \g_total_score_int {#1}
      \int_gincr:N \g_number_of_scores_int
    \fi
  }
}
% print row #1 of the part scores in the grade table
\cs_new:Nn \__add_row_to_grade_table:n {
   \tl_gput_right:Nx \g_grade_table_tl {\seq_item:Nn \g_part_scores_seq {#1}}
   \tl_gput_right:Nn \g_grade_table_tl { & }
   \tl_gput_right:Nx \g_grade_table_tl {\seq_item:Nn \g_part_scores_seq {#1+\g_number_of_scores_int/2}}
   \tl_gput_right:Nn \g_grade_table_tl {\\\hline}
}
\NewDocumentCommand\GradeTable{}{% the new grade table
  % we need an exam number of scores so add two
  % empty cells if we have an odd number
  \int_if_odd:nT {\g_number_of_scores_int} {
      \seq_gput_right:Nn \g_part_scores_seq {&}
      \int_ginc:N \g_number_of_scores_int
  }
  \int_gset:Nn \g_number_of_scores_int {\g_number_of_scores_int}
  \int_gadd:Nn \g_total_score_int { \multiplechoice }
  \int_gadd:Nn \g_total_score_int { \freeresponse }
  % create the grade table
  \tl_gclear:N \g_grade_table_tl
  \int_step_function:nnN {1} {\g_number_of_scores_int/2} \__add_row_to_grade_table:n
  \begin{tabular}{|c|c|c|c|c|c|}\hline\rowcolor{gray!20}
    Question & Points~Possible & Points~Earned & Question & Points~Possible & Points~Earned \\\hline
    \tl_use:N \g_grade_table_tl
    \multicolumn2{c|}{}&\multicolumn{2}{r|}{Multiple~Choice}
        & \multiplechoice & \\\cline{3-6}
    \multicolumn2{c|}{}&\multicolumn{2}{r|}{Free~response}
        & \freeresponse   & \\\cline{3-6}
    \multicolumn2{c|}{}&\multicolumn{2}{r|}{\textit{Exam~total}}
        & \int_use:N \g_total_score_int & \\\cline{3-6}
  \end{tabular}
}
\ExplSyntaxOff
\makeatother

\begin{document}

  \begin{questions}
    \question
      What if there were no air?
      \begin{parts}
        \part[4]
        Describe the effect on the balloon industry.
        \part[6]
        Describe the effect on the aircraft industry.
      \end{parts}

    \question
      \begin{parts}
        \part[12]
        Define the universe.
        Give three examples.
        \part[8]
        If the universe were to end, how would you know?
      \end{parts}
  \end{questions}

  \GradeTable

\end{document}

EDIT

Here is updated (and streamlined) version of the code above that saves the data to the aux file and reads it back in to construct the grade table. This allows you to put the table anywhere you like, including at the start of the document but also means that you will have to LaTeX the file twice before you see any scores.

After compiling the document two or more times, the updated MWE gives the following output:

enter image description here

This is much the same as before except that the table is now at the top of the document. If you compile the document only once then the grades table will have no scores for individuals questions, or their parts, and the total will be 0. Here is the updated code:

\documentclass[addpoints]{exam}

\usepackage[table]{xcolor}
\usepackage{xparse,xpatch,etoolbox}

% redefine \question command to be \myquest
\appto\questions{\let\examquestion\question\let\question\myquestion}
% redefine \part command to be \mypart
\appto\parts{\let\exampart\part\let\part\mypart}

\def\multiplechoice{5}
\def\freeresponse{6}
\makeatletter
\ExplSyntaxOn
% this will become a sequence of the part numbers and scores
% like: 1a,10,1b,8,1c,9,2a,6,2b,8,3,12,4,14, ...
\clist_new:N \g_grades_clist
\clist_new:N \g_grades_aux_clist

\int_new:N \g_row_int
\int_new:N \g_multiple_choice_int
\int_new:N \g_free_response_int
\int_new:N \g_grade_total_int
\int_new:N \g_number_of_scores_int

% add a question/part number and score to \g_grades_clist
\cs_new:Nn \__add_to_grades_list:nn {
  \clist_gput_right:Nx \g_grades_clist { #1 }
  \clist_gput_right:Nx \g_grades_clist { #2 }
}

\NewDocumentCommand\myquestion{o}{
  \IfNoValueTF{#1}{\examquestion}{
    % don't do anything special inside solutions
    \if@insolution\examquestion[#1]
    \else\examquestion[#1]
      % store both the part number and score in \g_grades_clist
      \__add_to_grades_list:nn { \arabic{question} } { #1 }
    \fi
  }
}

\NewDocumentCommand\mypart{o}{
  \IfNoValueTF{#1}{\exampart}{
    % don't do anything special inside solutions
    \if@insolution\exampnrt[#1]
    \else\exampart[#1]
      % store both the part number and score in \g_grades_clist
      \__add_to_grades_list:nn { \arabic{question}\alph{partno} } { #1 }
    \fi
  }
}

\AtEndDocument{
  \iow_now:cx { @auxout } {
    \token_to_str:N \SetGradeList { \g_grades_clist  } ^^J
    \token_to_str:N \SetMultipleChoice {\multiplechoice} ^^J
    \token_to_str:N \SetFreeResponse   {\freeresponse} ^^J
  }
}
% set grade list, multiple choice and free responses from the aux file
\NewDocumentCommand\SetGradeList{m}{\clist_gset:Nn \g_grades_aux_clist {#1}}
\NewDocumentCommand\SetMultipleChoice{m}{\int_gset:Nn \g_multiple_choice_int {#1}}
\NewDocumentCommand\SetFreeResponse{m}{\int_gset:Nn \g_free_response_int {#1}}
% print row #1 of the part scores in the grade table
\cs_new:Nn \__add_row_to_grade_table: {
     \int_gincr:N \g_row_int
       \clist_item:Nn \g_grades_aux_clist {2*\g_row_int-1}
      &\clist_item:Nn \g_grades_aux_clist {2*\g_row_int}
       \int_gadd:Nn \g_grade_total_int {\clist_item:Nn \g_grades_aux_clist {2*\g_row_int}}
     &&
     \int_compare:nTF {2*\g_row_int+\g_number_of_scores_int <= \clist_count:N \g_grades_aux_clist }{
       \clist_item:Nn \g_grades_aux_clist {2*\g_row_int+\g_number_of_scores_int-1}
      &\clist_item:Nn \g_grades_aux_clist {2*\g_row_int+\g_number_of_scores_int}
        \int_gadd:Nn \g_grade_total_int {\clist_item:Nn \g_grades_aux_clist {2*\g_row_int+\g_number_of_scores_int}}
     }{&}
     &\\\hline
     \int_compare:nT {\g_row_int < \g_number_of_scores_int/2} { \__add_row_to_grade_table: }
}
\NewDocumentCommand\PrintGradeTable{}{% the new grade table
  % we need an exam number of scores so add two
  % empty cells if we have an odd number
  \int_set:Nn \g_number_of_scores_int {(\clist_count:N \g_grades_aux_clist)/2}
  \int_if_odd:nT {\g_number_of_scores_int} {
      \int_add:Nn \g_number_of_scores_int {1}
  }
  \int_gzero:N \g_row_int % a counter to step through the rows
  \int_add:Nn \g_grade_total_int { \g_multiple_choice_int }
  \int_add:Nn \g_grade_total_int { \g_free_response_int }
  % create the grade table
  \begin{tabular}{|c|c|c|c|c|c|}\hline\rowcolor{gray!20}
    Question & Points~Possible & Points~Earned & Question & Points~Possible & Points~Earned \\\hline
    % the number of rows that we need is \g_number_of_scores_int/2
    \int_compare:nT {\g_number_of_scores_int>0} { \__add_row_to_grade_table: }
    \multicolumn2{c|}{}&\multicolumn{2}{r|}{Multiple~Choice}
        & \int_use:N \g_multiple_choice_int & \\\cline{3-6}
    \multicolumn2{c|}{}&\multicolumn{2}{r|}{Free~response}
        & \int_use:N \g_free_response_int   & \\\cline{3-6}
    \multicolumn2{c|}{}&\multicolumn{2}{r|}{\textit{Exam~total}}
        & \int_use:N \g_grade_total_int & \\\cline{3-6}
  \end{tabular}
}
\ExplSyntaxOff
\makeatother

\begin{document}

  \PrintGradeTable

  \begin{questions}
    \question
      \begin{parts}
        \part[1]
        \part[2]
      \end{parts}
    \question
      \begin{parts}
          \part[4]
          \part[2]
      \end{parts}
    \question
      \begin{parts}
          \part[2]
          \part[4]
          \part[4]
          \part[1]
        \end{parts}
    \question[5]
  \end{questions}

\end{document}

The code is probably simpler than before. The main changes are:

  • As in the new MWE, I added \myquestion and a new command \__add_to_grades_list:nn to add the question/part label and the score to tyhe grade list table
    • Instead of using sequences I am now using clists (=comma separated lists) as this works better with the aux file
    • the grades data is saved to the auxfile and then read back into \g_grades_aux_clist
    • The grade table is now printed in place
    • I am still hard coding the marks for the multiple choice and free response questions but I have added code to save these values to the aux file
    • new version that does a bare-hands loop so as to avoid potential issues with overleaf and TeXLive 2019 not yet being ported to ubuntu

After compiling the document once you will find the following lines in the aux file:

\SetGradeList{1a,1,1b,2,2a,4,2b,2,3a,2,3b,4,3c,4,3d,1,4,5}
\SetMultipleChoice{5}
\SetFreeResponse{6}

This the data that is used to construct the grade table.

\SetFreeResponse{6}