TransWikia.com

How to dynamically create a cumulative score table with hyperlinks?

TeX - LaTeX Asked on August 23, 2021

Scenario

I want to bundle students’ homeworks in a single PDF. The students submit their works in PNG and I grade them. Each homework is separated by a folder indicating the DueDate. For example, for homework that dues on 2020-07-16, there is a folder named 2020-07-16. The folder contains the students’ graded homeworks.

To save spaces, I encode the student names, for example, A10, X02, P33 and X04. The homework that dues on 2020-07-16 consists of 3 problems: "Simple", "Intermediate" and "Andvanced".

In folder 2020-07-16, there are

  • A10-1.png with score 4.5 (of 5).

  • X02-1.png with score 5 (of 5).

  • P33-1.png with score 2.5 (of 5).

  • X04-1.png with score 3.3 (of 5).

  • A10-2.png with score 2 (of 5).

  • X02-2.png with score 2 (of 5).

  • X04-2.png with score 2.3 (of 5).

  • A10-3.png with score 1 (of 5).

  • X02-3.png with score 2 (of 5).

  • P33-3.png with score 3 (of 5).

  • X04-3.png with score 4 (of 5).

As you can see, P33 does not submit his work on problem 2. In this case, he should get zero automatically.

User Interfaces

subsection is used to distinguish one due date from others while subsubsection is used to distinguish each problem in the same due date.

I define sx below because it corresponds to the due date folder.

letoldsubsectionsubsection
renewcommand{subsection}[2][]{defsx{#2}oldsubsection[#1]{#2}}

sx is used in score as follows.

newcommandscore[2]{%
    % #1 student codename
    % #2 score
    begin{center}
    includegraphics{sx/#1-arabic{subsubsection}}
    captionof{figure}{#1: #2}
    end{center}}

score takes two arguments: student encoded name and his score.

For example, in topic Solving With Factorization Method there is a homework that dues on 2020-07-16. I need to define the input file as follows.

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}

Questions

I want to dynamically generate the following cumulative score table where

  • each score cell is hyperlinked to its figure caption and vice versa.

  • each problem number is also hyperlinked to the corresponding subsection and vice versa.

  • Sub Average score is automatically calculated from total score divided by total problems in the same due date.

  • Total Average score is also automatically calculated from the average of Sub Average.

section{Cumulative Score Table}
begin{landscape}
begin{longtable}{|m{20mm}|m{5mm}|*4{m{10mm}|}}hline
Deadline        & No. & A10     & X02   & P33       & X04 hlinehline
2020-07-16  & 1     & 4.5   & 5         & 2.5       & 3.3hline
                        & 2     & 2         & 2         & fcolorbox{black}{yellow}{0}  & 2.3hline
                        & 3     & 1         & 2         & 3             & 4hline
Sub Avg         & 2     & 2.5   & 3         & 1.83      & 3.2hlinehline
Total Avg       & 2     & 2.5   & 3         & 1.83      & 3.2hline
end{longtable}
end{landscape}

"Dynamically" means I have to be freed from typing the table above by hands.
How to do so?

MWE

documentclass[demo,12pt]{article}
usepackage{xcolor}
usepackage{graphicx}
usepackage[a6paper,hmargin=5mm,top=5mm,bottom=13mm]{geometry}
usepackage[labelformat=empty]{caption}
usepackage{longtable}
usepackage{array}
usepackage{capt-of}

letoldsubsectionsubsection
renewcommand{subsection}[2][]{defsx{#2}oldsubsection[#1]{#2}}




newcommandscore[2]{%
    % #1 student codename
    % #2 score
    begin{center}
    includegraphics{sx/#1-arabic{subsubsection}}
    captionof{figure}{#1: #2}
    end{center}}

usepackage{pdflscape}
usepackage[colorlinks]{hyperref}

begin{document}


section{Cumulative Score Table}
begin{landscape}
begin{longtable}{|m{20mm}|m{5mm}|*4{m{10mm}|}}hline
Deadline        & No. & A10     & X02   & P33       & X04 hlinehline
2020-07-16  & 1     & 4.5   & 5         & 2.5       & 3.3hline
                        & 2     & 2         & 2         & fcolorbox{black}{yellow}{0}  & 2.3hline
                        & 3     & 1         & 2         & 3             & 4hline
Sub Avg         & 2     & 2.5   & 3         & 2.83      & 3.2hlinehline
Total Avg       & 2     & 2.5   & 3         & 2.83      & 3.2hline
end{longtable}
end{landscape}

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}

end{document}

3 Answers

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}
  {normalfontlargebfseries}{thesubsection}{1em}
  { % before code
    immediatewrite@auxout{stringcst@record@exercise{#1}}
    gdefcst@current@exercise{#1}
    #1
  }
  [ % after code
    label{sec:exer#1}
  ]

titleformat{subsubsection}
  {normalfontnormalsizebfseries}{thesubsubsection}{1em}
  { % before code
    immediatewrite@auxout{
      stringcst@record@problem{cst@current@exercise}{numberc@subsubsection}
    }
    xdefcst@current@problem{numberc@subsubsection}
    #1
  }
  [
    label{sec:exercst@[email protected]@current@problem}
  ]

letcst@current@exercise=@empty
letcst@current@problem =@empty


%%
%% user interface
%%

% #1 student
% #2 score
newcommandscore[2]{
  % write cst@record@score{<exercise>}{<problem>}{<student>}{<score>} to auxhy
  immediatewrite@auxout{
    stringcst@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:exercst@[email protected]@[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#1endcsname
    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#2endcsname
    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#3endcsname
  else
    tl_new:c {cst.stud#3}
    clist_gput_right:Nn l_cst_student_clist {#3}
  fi
  
  ifcsname cst.exer#1.prob#2.stud#3endcsname
    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 {  hlinehline }
    
    % 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{
  letcst@record@exercise=@gobble
  letcst@record@problem=@gobbletwo
  letcst@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

Correct answer by muzimuzhi Z on August 23, 2021

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}

letoldsubsectionsubsection
renewcommand{subsection}[2][]{defsx{#2}oldsubsection[#1]{#2}}

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

usepackage{datatool}

%workaround for unmatching pairs of braces within immediate
usepackage{newverbs}
Verbdefleftb|{|
Verbdefrightb|}|

%list of student names
defStudentNames{{"A10","X02","P33","X04"}}
defStudentNamesS{A10,X02,P33,X04}

%a temp file to store the generated tab
newwritemytmpfile
immediateopenoutmytmpfile=tmpFile.tex

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

% #1 list of scores
newcommandscoreN[1]{%
    ifthenelse{equal{theNoCounter}{1}}{
        immediatewritemytmpfile{sxunexpanded{ & hyperlink}{sx:theNoCounter}{theNoCounter}}
    }{
        immediatewritemytmpfile{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}}{
            immediatewritemytmpfile{
                unexpanded{ & fcolorbox{black}{yellow}}
                leftbunexpanded{hyperlink}
                {sx-StudentName-theNoCounter}{0}
                rightb}
        }{
            immediatewritemytmpfile{unexpanded{ & hyperlink}{sx-StudentName-theNoCounter}{x}}
        }
        DTLnewdbentry{SubAvgTab}{StudentName}{x}
    }
    immediatewritemytmpfile{unexpanded{hline}}
    stepcounter{NoCounter}
}

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

newcommandfinishTotal{%
    immediatewritemytmpfile{unexpanded{hlinehline}Total Avgunexpanded{ & }}
    %calc average for each column in SubAvgTab
    foreach x in StudentNamesS {
        DTLmeanforcolumn{TotalAvgTab}{x}{menaX}
        immediatewritemytmpfile{unexpanded{ & pgfmathprintnumber[fixed,precision=2]}{menaX}}
    }
    immediatewritemytmpfile{unexpanded{hlinehline}}
}

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
immediatecloseoutmytmpfile % write the tmp file

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

end{document}

Answered by susis strolch on August 23, 2021

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 = "makeatletternbegin{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}nmakeatothern"
        
        return tex_str
    end
    
end{luacode*}


newcommand{score}[2]{
    directlua{append_student_info("luaescapestring{#1}", "luaescapestring{#2}")}
    defimgfilename{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.4linewidth]{example-image}
            caption{#1: #2}
            label{directlua{tex.print(get_label{"fig", subsection_name, subsubsection_name, "luaescapestring{#1}"})}}
        end{figure}
    }
}

% reset section commands
letoldsubsectionsubsection
letoldsubsubsectionsubsubsection


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.5linewidth]{%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}nhlinen' % 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 'nn'.join(row1_all) + 'nn'.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

Answered by Alan Xiang on August 23, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP