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}
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 calledstudent_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"]
is4.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.)
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:
- Save students' information in an CSV file (you can work with Microsoft Excel).
- Use Python's
pandas
,numpy
to process data andos.path
to manage files. - Construct the TeX source in Python. It should be fairly easy with the help of Python's powerful string library and pylatex.
- 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}