How to dynamically create a cumulative score table with hyperlinks?

The full example is rather long and I'm sure there are optimize spaces, with only one user command \printScoreTable newly provided.

  • The syntax of \score is unchanged.
  • To patch sectioning commands, package titlesec is loaded.
  • The implementation can be naturally split into parts:
    • write info to aux,
    • collect info when aux is input by \begin{document},
    • print score table and cell formatter,
    • and finally handle the situation when aux is input by \end{document}.
  • Apart from your requirements,
    • the number of problems per exercise is auto calculated;
    • the full list of student codenames is auto accumulated;
    • duplicate exercises, duplicate questions under same exercises, and duplicate students under same questions are detected;
    • there is no restriction on the input order of students per question. For example, you can input \score{A01}{...} \score{A02}{...} for one problem, and input \score{A02}{...} \score{A01}{...} for another.
    • different link colors are used for link to exercises, problems, and figure captions.
    • time and space requirement are both linear, compared to the total number of score cells to be print

Some self-talk: LaTeX can do programming and data processing, but is not very good at. In general and in more complex situations, my suggestion is,

  • define appropriate markup commands in LaTeX,
  • then use another script language (for example Python) to process data and output a tex file making use of those markup command,
  • and finally input the tex file in LaXeX and produce PDF output.
\documentclass[demo,12pt]{article}

% normal packages in lexicographical order
\usepackage{array}
%\usepackage[labelformat=empty]{caption}
\usepackage{caption}
\usepackage[a6paper,hmargin=5mm,top=5mm,bottom=13mm]{geometry}
\usepackage{graphicx}
\usepackage{longtable}
\usepackage[explicit]{titlesec}
\usepackage{xcolor}

% special package
\usepackage[colorlinks]{hyperref}

\ExplSyntaxOn
\makeatletter

% use uniform prefix "cst", cumulative score table

%%
%% write info to aux
%%
\titleformat{\subsection}
  {\normalfont\large\bfseries}{\thesubsection}{1em}
  { % before code
    \immediate\write\@auxout{\string\cst@record@exercise{#1}}
    \gdef\cst@current@exercise{#1}
    #1
  }
  [ % after code
    \label{sec:exer#1}
  ]

\titleformat{\subsubsection}
  {\normalfont\normalsize\bfseries}{\thesubsubsection}{1em}
  { % before code
    \immediate\write\@auxout{
      \string\cst@record@problem{\cst@current@exercise}{\number\c@subsubsection}
    }
    \xdef\cst@current@problem{\number\c@subsubsection}
    #1
  }
  [
    \label{sec:exer\cst@[email protected]\cst@current@problem}
  ]

\let\cst@current@exercise=\@empty
\let\cst@current@problem =\@empty


%%
%% user interface
%%

% #1 student
% #2 score
\newcommand\score[2]{
  % write \cst@record@score{<exercise>}{<problem>}{<student>}{<score>} to auxhy
  \immediate\write\@auxout{
    \string\cst@record@score
      {\cst@current@exercise}{\cst@current@problem}{#1}{#2}
  }
  \begin{center}
    % image path: ./<exercise>/<student>-<problem>.png
    \includegraphics{\cst@exercise/#1-\arabic{subsubsection}}    
    \captionof{figure}{#1:~ #2}\label{fig:exer\cst@[email protected]\cst@[email protected]#1}
  \end{center}
}


%%
%% collect info when aux is input by \begin{document}
%%
\clist_new:N \l_cst_exercise_clist
\clist_new:N \l_cst_student_clist

\tl_new:N \l_cst_table_tl
\fp_new:N \l_cst_score_temp_fp
\int_new:N \l_cst_problems_count_int
\int_new:N \l_cst_total_problems_count_int

% #1 = exercise
\newcommand{\cst@record@exercise}[1]{
  % TODO: use \tl_if_exist:cTF?
  \ifcsname cst.exer#1\endcsname
    \PackageError{cst}
      {Exercise~ with~ due~ date~ "#1"~ multiply~ specified}{}
  \else
    \tl_new:c {cst.exer#1}
    % a problem clist per exercise
    % this allows different exercises having different number of problems
    \clist_new:c {l_cst_exer#1_problem_clist}
    \clist_gput_right:Nn \l_cst_exercise_clist {#1}
  \fi
}

% #1 = exercise
% #2 = problem
\newcommand{\cst@record@problem}[2]{
  \ifcsname cst.exer#1.prob#2\endcsname
    \PackageError{cst}
      {Problem~ "#2"~ under~ Exercise~ "#1"~ multiply~ specified}{}
  \else
    \tl_new:c {cst.exer#1.prob#2}
    \clist_gput_right:cn {l_cst_exer#1_problem_clist} {#2}
  \fi
}

% #1 = exercise due date
% #2 = problem serial number
% #3 = student codename
% #3 = score
\newcommand{\cst@record@score}[4]{
  % record a student list in \l_cst_student_clist, without duplicates
  \ifcsname cst.stud#3\endcsname
  \else
    \tl_new:c {cst.stud#3}
    \clist_gput_right:Nn \l_cst_student_clist {#3}
  \fi
  
  \ifcsname cst.exer#1.prob#2.stud#3\endcsname
    \PackageError{cst}
      {Score~ for~ student~ "#3'',~ Problem~ "#2",~ Exercise~ "#1"~ multiply~ specified}{}
  \else
    \tl_new:c {cst.exer#1.prob#2.stud#3}
    \tl_gset:cn {cst.exer#1.prob#2.stud#3} {#4}
  \fi
}


%%
%% print score table
%%
\cs_new:Npn \printScoreTable
  {
    % store table environment
    \tl_clear:N \l_cst_table_tl

    % for every student, create two fp
    \clist_map_inline:Nn \l_cst_student_clist
      {
        % sum of scores for all exercises
        \fp_new:c {l_cst_stud##1_fp}
        % sum of scores per exercise
        \fp_new:c {l_cst_stud##1_per_exercise_fp}
      }
    
    % table begin
    \tl_put_right:Nn \l_cst_table_tl { \begin{longtable} }
    
    % table preamble
    \tl_put_right:Nx \l_cst_table_tl 
      { {|m{20mm}|m{5mm}|*{\clist_count:N \l_cst_student_clist}{m{10mm}|}} }
    
    % table first row
    \tl_put_right:Nn \l_cst_table_tl { \hline }
    \tl_put_right:Nn \l_cst_table_tl { Deadline & No. }
    \clist_map_inline:Nn \l_cst_student_clist
      {
        \tl_put_right:Nn \l_cst_table_tl { & ##1 }
      }
    \tl_put_right:Nn \l_cst_table_tl { \\ \hline\hline }
    
    % for every exercise
    % convention: 
    % ##1: current exercise, ####1: current problem, \l_cst_curr_student_tl
    \clist_map_inline:Nn \l_cst_exercise_clist
      {
        % init per exercise accumulator
        \clist_map_variable:NNn \l_cst_student_clist \l_cst_curr_student_tl
          {
            \fp_zero:c {l_cst_stud \l_cst_curr_student_tl _per_exercise_fp}
          }
        
        % get number of problems in current exercise
        \int_set:Nn \l_cst_problems_count_int
          { \clist_count:c {l_cst_exer##1_problem_clist} }
        % and add it to total count of problems
        \int_add:Nn \l_cst_total_problems_count_int
          { \l_cst_problems_count_int }

        \cst_print_exercise_name:n { ##1 }
        
        % for every problem
        \clist_map_inline:cn {l_cst_exer##1_problem_clist}
          {
            \cst_print_problem_name:nn { ##1 } { ####1 }
            
            % for every student
            % use map_variable instead of map_inline, to get rid of ########1
            \clist_map_variable:NNn \l_cst_student_clist \l_cst_curr_student_tl
              {
                \tl_set_eq:Nc \l_cst_curr_score_tl 
                  {cst.exer##1.prob####1.stud \l_cst_curr_student_tl}
                \tl_if_exist:NTF \l_cst_curr_score_tl
                  { % if submitted
                    \fp_add:cn {l_cst_stud \l_cst_curr_student_tl _fp} {\l_cst_curr_score_tl}
                    \fp_add:cn {l_cst_stud \l_cst_curr_student_tl _per_exercise_fp} {\l_cst_curr_score_tl}

                    \cst_print_score:nnxx
                      {##1} {####1}
                      {\l_cst_curr_student_tl} {\l_cst_curr_score_tl}
                  }
                  { % unsubmitted
                    \cst_print_score_unsubmitted:
                  }
              } % end of every student
            \tl_put_right:Nn \l_cst_table_tl { \\ \hline }
          } % end of every problem
        
        % Sub Avg row
        \tl_put_right:Nn \l_cst_table_tl { Sub~ Avg & } % first two cells
        \clist_map_variable:NNn \l_cst_student_clist \l_cst_curr_student_tl
          {
            \fp_set:Nn \l_cst_score_temp_fp 
              {
                round(
                  \fp_use:c {l_cst_stud \l_cst_curr_student_tl _per_exercise_fp} /
                  \l_cst_problems_count_int
                , 2) % round to 2 places
              }

            \tl_put_right:Nn \l_cst_table_tl { & }
            \tl_put_right:Nx \l_cst_table_tl
             {
               \fp_to_decimal:N \l_cst_score_temp_fp
             }
          }
        \tl_put_right:Nn \l_cst_table_tl { \\ \hline }
      } % end of every exercise
    
    % Total Avg row
    \tl_put_right:Nn \l_cst_table_tl { \hline Total Avg & }
    \clist_map_variable:NNn \l_cst_student_clist \l_cst_curr_student_tl
      {
        \fp_set:Nn \l_cst_score_temp_fp
          {
            round(
              \fp_use:c {l_cst_stud \l_cst_curr_student_tl _fp} /
              \l_cst_total_problems_count_int
            , 2)
          }
        
        \tl_put_right:Nn \l_cst_table_tl { & }
        \tl_put_right:Nx \l_cst_table_tl
          {
            \fp_to_decimal:N \l_cst_score_temp_fp
          }
      }
    \tl_put_right:Nn \l_cst_table_tl { \\ \hline }
    
    % table end
    \tl_put_right:Nn \l_cst_table_tl { \end{longtable} }
    
    % print table
    \l_cst_table_tl
  }

%%
%% cell formatter
%%
\cs_new:Nn \cst_print_exercise_name:n
  {
    % exercise is always the first cell in a table row, so no need to put &
    \tl_put_right:Nn \l_cst_table_tl
      { 
        % every table cell is already inside a group, 
        % so the change to \@linkcolor is ensured locel
        \tl_set:Nn \@linkcolor {blue}
        % syntax: \hyperref[<label>]{<text>}
        \hyperref[sec:exer#1]{#1}
      }
  }

% #1 = exercise
% #2 = problem
\cs_new:Nn \cst_print_problem_name:nn
  {
    \tl_put_right:Nn \l_cst_table_tl
      {
        & \tl_set:Nn \@linkcolor {teal}
        \hyperref[sec:exer#1.prob#2]{#2}
      }
  }

% #1 = exercise
% #2 = problem
% #3 = student
% #4 = score
\cs_new:Nn \cst_print_score:nnnn
  {
    \tl_put_right:Nn \l_cst_table_tl
      {
        & \hyperref[fig:exer#1.prob#2.stud#3]{#4}
      }
  }
\cs_generate_variant:Nn \cst_print_score:nnnn {nnxx}

\cs_new:Nn \cst_print_score_unsubmitted:
  {
    \tl_put_right:Nn \l_cst_table_tl
      {
        & \fcolorbox{black}{yellow}{0}
      }
  }

%%
%% handle the situation when aux is input by \end{document}
%%

\AtEndDocument{
  \let\cst@record@exercise=\@gobble
  \let\cst@record@problem=\@gobbletwo
  \let\cst@record@score=\@gobblefour
}

\makeatother
\ExplSyntaxOff


\begin{document}

\section{Cumulative Score Table}
\printScoreTable

\section{Solving With Factorization Method}

\subsection{2020-07-16}

\subsubsection{Simple}
\score{A10}{1}
\score{X02}{5}
\score{P33}{2.5}
\score{X04}{3.3}

\subsubsection{Intermediate}
\score{A10}{2}
\score{X02}{2}
% Here student P33 does not submit the solution.
\score{X04}{2.3}

\subsubsection{Advanced}
\score{A10}{3}
\score{X02}{2}
\score{P33}{3}
\score{X04}{4}

\subsection{2020-08-01}

\subsubsection{Simple}
\score{A10}{4}
% Here student X02 does not submit the solution.
\score{P33}{4.4}
\score{X04}{5}

\subsubsection{Intermediate}
\score{A10}{5}
\score{X02}{1}
\score{P33}{2}
\score{X04}{4}

\end{document}

enter image description here


A more "creative answers" with slightly changed input of your scores and assuming that you enter the scores for each student in always the same order (fixed list of student names) bring me to this solution:

While entering the scores for a new subsubsection with \scoreN{{4.5,5,2.5,3.3}} a new line in an external file is written where I store the generated table. At the same time a line in a datatool table is generated. When all subsubsection area finished (a date/subsection) is finished the Sub Avg values are calculate using DTLmeanforcolumn and again a line in the generated tab is written and a line in a secund datatool tab is wirtten to store the Suv Avg values. After all dates are finished the last line of the generated tab is written usin the TotalAvg tab to calculate the total averages. At the end of the document the generated external file is simply included by \input{tmpFile.tex}.

For sure the code could be further optimized but it seams to work. Btw., I force to add a zero for students which did not submit a solution and I also plot an image for this case. The idea for this was that there will be students having submitted a solution but got zero points for it - so I want to see the solution when I click the link in the table.

\documentclass[demo,12pt]{article}
\usepackage{xcolor}
\usepackage{graphicx}
\usepackage[labelformat=empty]{caption}
\usepackage{longtable}
\usepackage{array}
\usepackage{capt-of}

\let\oldsubsection\subsection
\renewcommand{\subsection}[2][]{\def\sx{#2}\oldsubsection[#1]{#2}}

\usepackage{tikz} % for foreach, pfgmath...
\usepackage{ifthen}

\usepackage{datatool}

%workaround for unmatching pairs of braces within \immediate
\usepackage{newverbs}
\Verbdef\leftb|{|
\Verbdef\rightb|}|

%list of student names
\def\StudentNames{{"A10","X02","P33","X04"}}
\def\StudentNamesS{A10,X02,P33,X04}

%a temp file to store the generated tab
\newwrite\mytmpfile
\immediate\openout\mytmpfile=tmpFile.tex

%probelm number counter 
\newcounter{NoCounter}
\setcounter{NoCounter}{1}

% #1 list of scores
\newcommand\scoreN[1]{%
    \ifthenelse{\equal{\theNoCounter}{1}}{
        \immediate\write\mytmpfile{\sx\unexpanded{ & \hyperlink}{\sx:\theNoCounter}{\theNoCounter}}
    }{
        \immediate\write\mytmpfile{\unexpanded{ & \hyperlink}{\sx:\theNoCounter}{\theNoCounter}}
    }
    
    \dtlexpandnewvalue
    \DTLnewrow{SubAvgTab}
    \foreach \x [count=\xi from 0] in #1 {
        \begin{center}
        \includegraphics{\sx/\x-\arabic{subsubsection}}
        \pgfmathsetmacro{\StudentName}{\StudentNames[\xi]}
        \captionof{figure}{\StudentName: \x}\hypertarget{\sx-\StudentName-\theNoCounter}{}
        \end{center}
        
        \pgfmathsetmacro{\StudentName}{\StudentNames[\xi]} % id do not know why i have to repeat this here (but without the \StudentName definition is not known)
        \ifthenelse{\equal{\x}{0}}{
            \immediate\write\mytmpfile{
                \unexpanded{ & \fcolorbox{black}{yellow}}
                \leftb\unexpanded{\hyperlink}
                {\sx-\StudentName-\theNoCounter}{0}
                \rightb}
        }{
            \immediate\write\mytmpfile{\unexpanded{ & \hyperlink}{\sx-\StudentName-\theNoCounter}{\x}}
        }
        \DTLnewdbentry{SubAvgTab}{\StudentName}{\x}
    }
    \immediate\write\mytmpfile{\unexpanded{\\\hline}}
    \stepcounter{NoCounter}
}

\newcommand\finishDate{%
    %set problem number counter back
    \setcounter{NoCounter}{1}
    
    \dtlexpandnewvalue
    \DTLnewrow{TotalAvgTab}
    \immediate\write\mytmpfile{Sub Avg\unexpanded{ & }}
    %calc average for each column in SubAvgTab and write them to the tmp file
    \foreach \x in \StudentNamesS {
        \DTLmeanforcolumn{SubAvgTab}{\x}{\menaX}
        \immediate\write\mytmpfile{\unexpanded{ & \pgfmathprintnumber[fixed,precision=2]}{\menaX}}
        \DTLnewdbentry{TotalAvgTab}{\x}{\menaX}
    }
    \immediate\write\mytmpfile{\unexpanded{\\\hline\hline}}
    %clear SubAvgTab
    \DTLcleardb{SubAvgTab}
}

\newcommand\finishTotal{%
    \immediate\write\mytmpfile{\unexpanded{\hline\hline}Total Avg\unexpanded{ & }}
    %calc average for each column in SubAvgTab
    \foreach \x in \StudentNamesS {
        \DTLmeanforcolumn{TotalAvgTab}{\x}{\menaX}
        \immediate\write\mytmpfile{\unexpanded{ & \pgfmathprintnumber[fixed,precision=2]}{\menaX}}
    }
    \immediate\write\mytmpfile{\unexpanded{\\\hline\hline}}
}

\usepackage{pdflscape}
\usepackage[colorlinks]{hyperref}

\begin{document}
\DTLnewdb{SubAvgTab}
\DTLnewdb{TotalAvgTab}

\section{Solving With Factorization Method}

\subsection{2020-07-16}

\subsubsection{Simple}\hypertarget{\sx:\theNoCounter}{}
\scoreN{{4.5,5,2.5,3.3}}

\subsubsection{Intermediate}\hypertarget{\sx:\theNoCounter}{}
\scoreN{{2,2,0,2.3}}

\subsubsection{Advanced}\hypertarget{\sx:\theNoCounter}{}
\scoreN{{1,2,3,4}}

\finishDate
\subsection{2020-07-23}

\subsubsection{Simple}\hypertarget{\sx:\theNoCounter}{}
\scoreN{{2.5,4,3.5,4}}

\subsubsection{Intermediate}\hypertarget{\sx:\theNoCounter}{}
\scoreN{{0,4,3,3}}

\finishDate

\section{Generated Cumulative Score Table}
\finishTotal
\immediate\closeout\mytmpfile % write the tmp file

\begin{landscape}
\begin{longtable}{|m{20mm}|m{5mm}|*4{m{10mm}|}}\hline
Deadline        & No. & A10     & X02   & P33       & X04 \\\hline\hline
\input{tmpFile.tex}
\end{longtable}
\end{landscape}

\end{document}

About this solution

  • This solution is implemented with Lua. Therefore, the compiler must be LuaTeX.

  • Basically, a table object called student_info in Lua keeps tracks of all student information. For example, if one writes:

    \subsection{2020-07-16}
    \subsubsection{Simple}
    \score{A10}{4.5}
    

    Then the value of student_info["2020-07-16"]["Simple"]["A10"] is 4.5. Most of the code is in charge of transforming and formatting this table for output.

  • The table is constructed with generate_tex_table() function. Every time the document is compiled, the constructed table is saved to \jobname.mytable. The table can be shown by using \printtable command, which essentially reads \jobname.mytable.

    % update the table when document finishes
    \makeatletter
    \AtEndDocument{
      \directlua{
          out = io.open("\jobname.mytable", "w")
          out:write(generate_tex_table())
          io.close(out)
      }
    }
    \makeatother
    % try to retrieve the table generated from last run
    \newcommand{\printtable}{
      \InputIfFileExists{\jobname.mytable}
    }
    
  • A set of format_ functions controls how data are formatted in the output.

Source

\documentclass{article}
\usepackage{array}
\usepackage{newtxtext, newtxmath}
\usepackage{expl3}
\usepackage{luacode}
\usepackage{xcolor}
\usepackage{longtable}
\usepackage{float}
\usepackage{graphicx}
\usepackage{datetime2}
\usepackage[colorlinks]{hyperref}

\begin{document}

\directlua{
    % debug only
    % https://github.com/kikito/inspect.lua
    % inspect = require('inspect')
    
    subsection_name = ''
    subsubsection_name = ''
}

\begin{luacode*}
    student_info = {}
    
    function table_get(tb, key, default)
        if (tb[key] == nil) then
            tb[key] = default
        end
        return tb[key]
    end
    
    function get_table_size(t)
        local count = 0
        for _, __ in pairs(t) do
            count = count + 1
        end
        return count
    end
    
    -- using `get_table_size` to track the order of occurrence
    function append_student_info(name, score)
        local subsec_tb = table_get(student_info, subsection_name, {{}, get_table_size(student_info)})
        local subsubsec_tb = table_get(subsec_tb[1], subsubsection_name, {{}, get_table_size(subsec_tb[1])})
        subsubsec_tb[1][name] = tonumber(score)
    end
    
    -- sort keys by occurrence order
    function get_sorted_keys(tb)
        local arr = {}
        for key, val in pairs(tb) do
            local seq = val[2]
            arr[seq + 1] = key
        end
        return arr
    end
    
    function _get_all_student_name(tb, set)
        for key, val in pairs(tb) do
            if (type(val) == "table") then
                if (type(val[1]) == "table") then
                    _get_all_student_name(val[1], set)
                end
            elseif (type(val) == "number") then
                set[key] = true
            end
        end
    end
    
    -- get all student names in sorted order
    function get_all_student_name(tb)
        local set = {}
        _get_all_student_name(tb, set)
        
        local lst = {}
        for key, val in pairs(set) do
            table.insert(lst, key)
        end
        
        table.sort(lst)
        return lst
    end
    
    -- get tex label of something
    function get_label(l)
        return table.concat(l, "-")
    end
    
    -- format a score
    function format_score(ops)
        local score = ops[1]
        
        local no_img = false
        if (score == nil) then
            no_img = true
            score = 0.0
        end
        
        
        local num_str = string.format("%2.2f", score)
        local tex_str = ""
        
        if (math.abs(score) < 0.01) then
            tex_str = string.format("\\fcolorbox{black}{yellow}{%s}", num_str)
        else
            tex_str = num_str
        end
        
        if no_img or ops["no_link"] then
            return tex_str
        else
            local label = get_label{"fig", ops[2], ops[3], ops[4]}
            return [[\def\@linkcolor{red}]] .. string.format("\\hyperref[%s]{%s}", label, tex_str)
        end
    end
    
    function format_deadline(ops)
        local label = get_label{"subsec", ops[1]}
        return [[\def\@linkcolor{blue}]] .. string.format("\\hyperref[%s]{%s}", label, ops[1])
    end
    
    function format_no(ops)
        local label = get_label{"subsubsec", ops[2], ops[3]}
        return [[\def\@linkcolor{green}]] .. string.format("\\hyperref[%s]{%s}", label, ops[1])
    end
    
    function get_sub_average(subsec_name, student_name, subsubsec_names)
        local sum = 0.0
        local count = 0
        local tb = student_info[subsec_name][1]
        for ind, subsubsec_name in pairs(subsubsec_names) do
            local score = tb[subsubsec_name][1][student_name]
            if (score ~= nil) then
                sum = sum + score
            end
            count = count + 1
        end
        return sum / count
    end
    
    function get_total_average(student_name, subsec_names)
        local sum = 0.0
        local count = 0
        
        for _, subsec_name in pairs(subsec_names) do
            local subsec_tb = student_info[subsec_name]
            local subsubsec_names = get_sorted_keys(subsec_tb[1])
            for ind, subsubsec_name in pairs(subsubsec_names) do
                local score = subsec_tb[1][subsubsec_name][1][student_name]
                if (score ~= nil) then
                    sum = sum + score
                end
                count = count + 1
            end
        end
        
        return sum / count
        
    end
    
    function generate_tex_table()
        local student_names = get_all_student_name(student_info)
        
        local rows = {}
        
        local row = {"Deadline", "No."}
        for _, student_name in pairs(student_names) do
            table.insert(row, student_name)
        end
        table.insert(rows, row)
        
        local subsec_names = get_sorted_keys(student_info)
        for _, subsec_name in pairs(subsec_names) do
            local subsec_tb = student_info[subsec_name]
            local subsubsec_names = get_sorted_keys(subsec_tb[1])
            
            for ind, subsubsec_name in pairs(subsubsec_names) do
                local row = nil
                if (ind == 1) then
                    row = {format_deadline{subsec_name}, format_no{ind, subsec_name, subsubsec_name}}
                else
                    row = {'', format_no{ind, subsec_name, subsubsec_name}}
                end
                
                local subsubsec_tb = subsec_tb[1][subsubsec_name]
                for _, student_name in pairs(student_names) do
                    table.insert(row, format_score{subsubsec_tb[1][student_name], subsec_name, subsubsec_name, student_name})
                end
                
                table.insert(rows, row)
            end
            
            local row = {"Sub Avg", ""}
            for _, student_name in pairs(student_names) do
                table.insert(row, format_score{get_sub_average(subsec_name, student_name, subsubsec_names), no_link=true})
            end
            table.insert(rows, row)
        end
        
        row = {"Total Avg", ""}
        for _, student_name in pairs(student_names) do
            table.insert(row, format_score{get_total_average(student_name, subsec_names), no_link=true})
        end
        table.insert(rows, row)
        
        
        -- construct tex string
        local n_cols = get_table_size(rows[1])
        local table_fmt_tmp = {"m{20mm}", "m{5mm}"}
        for i = 3,n_cols do
            table.insert(table_fmt_tmp, "m{10mm}")
        end
        local table_fmt = "|" .. table.concat(table_fmt_tmp, "|") .. "|"
        local tex_str = "\\makeatletter\n\\begin{longtable}{" .. table_fmt .. "} \\hline \n"
        
        for _, row in pairs(rows) do
            local row_str = table.concat(row, " & ") .. "\\\\ \\hline \n"
            tex_str = tex_str .. row_str
        end
        
        tex_str = tex_str .. "\\end{longtable}\n\\makeatother\n"
        
        return tex_str
    end
    
\end{luacode*}


\newcommand{\score}[2]{
    \directlua{append_student_info("\luaescapestring{#1}", "\luaescapestring{#2}")}
    \def\imgfilename{\directlua{tex.print(subsection_name)}/#1-\arabic{subsubsection}}
    \IfFileExists{\imgfilename}{
        \begin{figure}[H]
            \centering
            \includegraphics{\imgfilename}
            \caption{#1: #2}
            \label{\directlua{tex.print(get_label{"fig", subsection_name, subsubsection_name, "\luaescapestring{#1}"})}}
        \end{figure}
    }{
        % this is for testing
        % maybe raise error if not found?
        \begin{figure}[H]
            \centering
            \includegraphics[width=0.4\linewidth]{example-image}
            \caption{#1: #2}
            \label{\directlua{tex.print(get_label{"fig", subsection_name, subsubsection_name, "\luaescapestring{#1}"})}}
        \end{figure}
    }
}

% reset section commands
\let\oldsubsection\subsection
\let\oldsubsubsection\subsubsection


\renewcommand{\subsection}[2][]{
    \directlua{
        subsection_name="\luaescapestring{#2}"
        local subsec_tb = table_get(student_info, subsection_name, {{}, get_table_size(student_info)})
    }
    \oldsubsection[#1]{#2}
    \label{\directlua{tex.print(get_label{"subsec", subsection_name})}}
}
\renewcommand{\subsubsection}[2][]{
    \directlua{
        subsubsection_name="\luaescapestring{#2}"
        local subsec_tb = table_get(student_info, subsection_name, {{}, get_table_size(student_info)})
        local subsubsec_tb = table_get(subsec_tb[1], subsubsection_name, {{}, get_table_size(subsec_tb[1])})
    }
    \oldsubsubsection[#1]{#2}
    \label{\directlua{tex.print(get_label{"subsubsec", subsection_name, subsubsection_name})}}
}


% update the table when document finishes
\makeatletter
\AtEndDocument{
    \directlua{
        out = io.open("\jobname.mytable", "w")
        out:write(generate_tex_table())
        io.close(out)
    }
}
\makeatother
% try to retrieve the table generated from last run
\newcommand{\printtable}{
    \InputIfFileExists{\jobname.mytable}
}


\printtable


\section{Solving With Factorization Method}

\subsection{2020-07-16}

\subsubsection{Simple}
\score{A10}{4.5}
\score{X02}{5}
\score{P33}{2.5}
\score{X04}{3.3}

\subsubsection{Intermediate}
\score{A10}{2}
\score{X02}{2}
% If, for example, P33 does not submit the solution
%  he should  get zero automatically.
% His score cell  on the cumulative table with
% missing homework must be highlighted with a unique color. 
\score{X04}{2.3}


\subsubsection{Advanced}
\score{A10}{1}
\score{X02}{2}
\score{P33}{3}
\score{X04}{4}


\subsection{2020-07-18}

\subsubsection{Simple}
\score{A10}{2}
\score{X02}{4.3}
\score{P33}{0}
\score{X04}{6.5}

\subsubsection{Intermediate}
\score{A10}{3}
\score{X02}{4}
\score{P33}{2}
\score{X04}{5}


\subsubsection{Advanced}
\score{A10}{4}
\score{X02}{5}
\score{P33}{2}
\score{X04}{1}

\subsubsection{Impossible}


\DTMNow

\end{document}

The table

(The design is inspired muzimuzhi Z's answer. By looking at his solution, I can't help but admire how good his LaTeX programming skills are...It is beautiful and he absolutely deserves the reward. My only concern is about the precision of LaTeX3's floating point arithmetic though. However, for this kind of application, it should be fine.)

luatex method

My thoughts

I was really fanatic about LaTeX programming a while ago, and I implemented a bunch of algorithms in LaTeX. However, when my passion begin to fade away, I started to contemplate: "do I really need to implement this/that in LaTeX"? The problem of LaTeX is that it is just not designed for generic programming. Yes, it is Turing complete, but the workload to implement simple algorithms in LaTeX can be huge.

In your specific case, you are trying to apply simple data processing and file management with LaTeX directly. I tried to avoid LaTeX3 by using Lua, but it turned out that Lua is not the best language for these purposes either: you may notice that I need to write explicit loop blocks for aggregating arrays, which is a one-liner in Python. In fact, the entire Lua code almost has nothing to do with LaTeX. That is to say, it can be written in any language.

If I were you, this is probably what I would do:

  1. Save students' information in an CSV file (you can work with Microsoft Excel).
  2. Use Python's pandas, numpy to process data and os.path to manage files.
  3. Construct the TeX source in Python. It should be fairly easy with the help of Python's powerful string library and pylatex.
  4. Save TeX source to a file and use subprocess.run to call TeX executables to compile the file.

If efficiency is the key, I think this approach can save a lot of time and the outcome will be much more customizable compared to pure LaTeX/LuaTeX solution. Nevertheless, I still had a lot of fun writing the code above. Hope you like my alternative solution

Using Python to generate LaTeX source

In this case, students information can be stored in a CSV file (supposed it is saved as Book1.csv).

Student,2020-07-15/Basic,2020-07-15/Intermediate,2020-07-15/Advanced,2020-07-18/Basic,2020-07-18/Intermediate,2020-07-18/Advanced,2020-07-18/Impossible
Lorem Ipsum,4.43,2.39,4.90,4.19,2.42,4.61,
Dolor Sit,3.03,4.13,3.18,4.58,3.97,1.12,
Amet Consectetuer,4.05,,,4.75,4.36,4.36,
Adipiscing Elit,1.80,3.03,1.55,4.50,2.69,3.68,
Aenean Commodo,3.99,4.55,4.37,,4.17,0.00,

Then, I can use the following Python code to generate TeX source.

import pandas
import numpy as np
from collections import OrderedDict
import re
import os

# to imitate dict's get() method for ordered dict
def od_get(od, key, default):
    if key in od:
        return od[key]
    od[key] = default
    return od[key]

class CSV2TeX:

    def __init__(self, csv_filename, **kwargs):
        self.df = pandas.read_csv(csv_filename)
        self.df_arr = self.df.iloc[:, 1:].to_numpy()
        self.section_title = kwargs.get('section_title', '')

        self._parse_header()
        # get all student names
        self.all_students = self.df.iloc[:, 0].tolist()

        # global variables to expedite formatting
        self.date = ''
        self.description = ''
        self.student = ''
        self.question_id = 0
        self.score = np.NaN
        self.score_no_link = False

    # parse headers - the mapping result in the index of a header item
    def _parse_header(self):
        self.header_info = OrderedDict()
        for ind, header_str in enumerate(self.df.columns.values):
            if ind == 0: continue
            date, description = header_str.split('/')
            od_get(self.header_info, date, OrderedDict())[description] = ind - 1

    def _get_label(self, *args):
        return re.sub(r'\s', '-', '-'.join(args)).lower()

    def format_date(self):
        label = self._get_label('subsec', self.date)
        return r'\def\@linkcolor{blue}\hyperref[%s]{%s}' % (label, self.date)

    def format_question_id(self):
        label = self._get_label('subsubsec', self.date, self.description)
        return r'\def\@linkcolor{green}\hyperref[%s]{%s}' % (label, self.question_id + 1)

    def format_score(self):
        score = self.score
        no_link = self.score_no_link
        score_fmt = '{:.2f}'
        if np.isnan(score):
            no_link = True
            score = 0.0
            score_s = r'\fcolorbox{black}{yellow}{%s}' % (score_fmt.format(score),)
        else:
            score_s = score_fmt.format(score)


        if no_link:
            return score_s

        label = self._get_label('figure', self.date, self.description, self.student)
        return r'\def\@linkcolor{red}\hyperref[%s]{%s}' % (label, score_s)

    def format_figure(self):
        label = self._get_label('figure', self.date, self.description, self.student)
        # TODO: change image_path for actual application
        image_path = 'example-image'
        #image_path = os.path.join(self.date, self.student + repr(self.question_id))
        caption = '{}: {}'.format(self.student, self.score)
        return r'''
\begin{figure}[H]
\centering
\includegraphics[width=0.5\linewidth]{%s}
\caption{%s}
\label{%s}
\end{figure}''' % (image_path, caption, label)

    def format_section(self):
        return r'\section{%s}' % (self.section_title,)

    def format_subsection(self):
        label = self._get_label('subsec', self.date)
        return r'\subsection{%s}\label{%s}' % (self.date, label)

    def format_subsubsection(self):
        label = self._get_label('subsubsec', self.date, self.description)
        return r'\subsubsection{%s}\label{%s}' % (self.description, label)

    def convert(self):
        rows1 = []  # the table
        rows2 = []  # figures

        rows1.append(['Deadline', 'No.'] + self.all_students)

        # generate the table
        for date, descriptions in self.header_info.items():
            self.date = date
            rows2.append(self.format_subsection())

            all_col_inds = []
            for q_ind, (description, col_ind) in enumerate(descriptions.items()):
                all_col_inds.append(col_ind)
                self.question_id = q_ind
                self.description = description
                rows2.append(self.format_subsubsection())

                if q_ind == 0:
                    row = [self.format_date(), self.format_question_id()]
                else:
                    row = ['', self.format_question_id()]

                for s_ind, student in enumerate(self.all_students):
                    self.student = student
                    self.score = self.df_arr[s_ind, col_ind]
                    row.append(self.format_score())
                    if not np.isnan(self.score):
                        rows2.append(self.format_figure())

                rows1.append(row)

            row = ['Sub Avg', '']
            # compute sub avg for each student
            self.score_no_link = True
            for s_ind, student in enumerate(self.all_students):
                student_cols = np.take(self.df_arr[s_ind, ...], all_col_inds)
                student_cols[np.isnan(student_cols)] = 0.0
                self.score = np.mean(student_cols)
                row.append(self.format_score())
            self.score_no_link = False
            rows1.append(row)

        # compute total avg for each student
        row = ['Total Avg', '']
        self.score_no_link = True
        for s_ind, student in enumerate(self.all_students):
            student_row = self.df_arr[s_ind, ...]
            student_row[np.isnan(student_row)] = 0.0
            self.score = np.mean(student_row)
            row.append(self.format_score())
        self.score_no_link = False
        rows1.append(row)

        # find the longest cell for pretty printing
        longest_len = max(map(lambda x : max(map(len, x)), rows1))
        fmt_str = '{:<%d}' % longest_len

        # generate tex table
        tex_table_fmt = '|' + '|'.join(['l'] * len(rows1[0])) + '|'
        row1_str = '\\begin{longtable}{%s}\n\\hline\n' % tex_table_fmt
        for row in rows1:
            row_str = ' & '.join(map(lambda x : fmt_str.format(x), row)) + r'\\ \hline ' + '\n'
            row1_str += row_str
        row1_str += '\\end{longtable}'

        row1_all = [self.format_section(), r'\makeatletter', row1_str, r'\makeatother', '\n']

        return '\n\n'.join(row1_all) + '\n\n'.join(rows2)

with open('my_table.tex', 'w') as outfile:
    outfile.write(CSV2TeX('Book1.csv', section_title='Math').convert())

The code above stores the LaTeX source into my_table.tex (the file is too long for this answer, you can peek at it from here). Now, I can compile the following document to get desired output. Of course, it is possible to fully automate this by using subprocess.

\documentclass{article}
\usepackage{array}
\usepackage{newtxtext, newtxmath}
\usepackage{luacode}
\usepackage{xcolor}
\usepackage{longtable}
\usepackage{float}
\usepackage{graphicx}
\usepackage{datetime2}
\usepackage[colorlinks]{hyperref}

\begin{document}

\input{my_table.tex}

\end{document}

python method

Tags:

Programming