设想
我想将学生的作业打包成一个 PDF。学生以 PNG 格式提交他们的作业,然后我给他们评分。每个作业都用一个文件夹分隔,并标明DueDate
。例如,对于 的作业2020-07-16
,有一个名为 的文件夹2020-07-16
。该文件夹包含学生的已评分作业。
为了节省空间,我对学生姓名进行了编码,例如,,A10
和X02
。所交作业包括 3 个问题:“简单”,“中级”和“高级”。P33
X04
2020-07-16
在文件夹中2020-07-16
,有
A10-1.png
得分 4.5 (满分 5 分)。X02-1.png
得分 5 (满分 5 分)。P33-1.png
得分 2.5 (满分 5 分)。X04-1.png
得分 3.3 (满分 5 分)。A10-2.png
得分为 2 (满分 5 分)。X02-2.png
得分为 2 (满分 5 分)。X04-2.png
得分 2.3 (满分 5 分)。A10-3.png
得分为 1 (满分 5 分)。X02-3.png
得分为 2 (满分 5 分)。P33-3.png
得分为 3 (满分 5 分)。X04-3.png
得分为 4 (满分 5 分)。
正如你所看到的,P33
他没有提交问题 2 的作业。在这种情况下,他应该自动获得零分。
用户界面
\subsection
用于区分一个截止日期与其他截止日期,而\subsubsection
用于区分同一截止日期内的各个问题。
我在下面定义\sx
,因为它对应于到期日文件夹。
\let\oldsubsection\subsection
\renewcommand{\subsection}[2][]{\def\sx{#2}\oldsubsection[#1]{#2}}
\sx
用法\score
如下。
\newcommand\score[2]{%
% #1 student codename
% #2 score
\begin{center}
\includegraphics{\sx/#1-\arabic{subsubsection}}
\captionof{figure}{#1: #2}
\end{center}}
\score
接受两个参数:学生编码的姓名和分数。
例如,在主题中Solving With Factorization Method
有一项作业是2020-07-16
。我需要定义输入文件如下。
\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}
问题
我想动态生成以下累积分数表,其中
每个分数单元都超链接到其图形标题,反之亦然。
每个问题编号也超链接到相应的小节,反之亦然。
Sub Average
分数是根据同一截止日期内的总分数除以总问题数自动计算出来的。Total Average
分数也是根据的平均值自动计算的Sub Average
。
\section{Cumulative Score Table}
\begin{landscape}
\begin{longtable}{|m{20mm}|m{5mm}|*4{m{10mm}|}}\hline
Deadline & No. & A10 & X02 & P33 & X04 \\\hline\hline
2020-07-16 & 1 & 4.5 & 5 & 2.5 & 3.3\\\hline
& 2 & 2 & 2 & \fcolorbox{black}{yellow}{0} & 2.3\\\hline
& 3 & 1 & 2 & 3 & 4\\\hline
Sub Avg & 2 & 2.5 & 3 & 1.83 & 3.2\\\hline\hline
Total Avg & 2 & 2.5 & 3 & 1.83 & 3.2\\\hline
\end{longtable}
\end{landscape}
“动态”意味着我必须从手动输入上面的表格中解放出来。怎么做呢?
平均能量损失
\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}
\let\oldsubsection\subsection
\renewcommand{\subsection}[2][]{\def\sx{#2}\oldsubsection[#1]{#2}}
\newcommand\score[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 \\\hline\hline
2020-07-16 & 1 & 4.5 & 5 & 2.5 & 3.3\\\hline
& 2 & 2 & 2 & \fcolorbox{black}{yellow}{0} & 2.3\\\hline
& 3 & 1 & 2 & 3 & 4\\\hline
Sub Avg & 2 & 2.5 & 3 & 2.83 & 3.2\\\hline\hline
Total Avg & 2 & 2.5 & 3 & 2.83 & 3.2\\\hline
\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}
答案1
完整的示例相当长,我确信有优化空间,只\printScoreTable
新提供了一个用户命令。
- 的语法
\score
没有改变。 - 为了修补分段命令,
titlesec
需要加载包。 - 实现可以自然地分为几个部分:
- 将信息写入辅助设备,
- 当 aux 输入时收集信息
\begin{document}
, - 打印分数表和单元格格式化程序,
- 最后处理 aux 输入的情况
\end{document}
。
- 除了您的要求外,
- 每个练习的问题数量是自动计算的;
- 学生代号的完整列表是自动累积的;
- 检测重复练习、同一练习下的重复题目、同一题目下的重复学生;
- 每道题的学生输入顺序没有限制,比如可以先输入
\score{A01}{...} \score{A02}{...}
一道题,再输入\score{A02}{...} \score{A01}{...}
另一道题。 - 不同的链接颜色用于链接到练习、问题和图形标题。
- 与要打印的分数单元总数相比,时间和空间要求都是线性的
一些自我对话:LaTeX 可以进行编程和数据处理,但不是很擅长。在一般情况下以及在更复杂的情况下,我的建议是,
- 在 LaTeX 中定义适当的标记命令,
- 然后使用另一种脚本语言(例如 Python)来处理数据并
tex
利用这些标记命令输出文件, - 最后将
tex
文件输入 LaXeX 并生成 PDF 输出。
\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}
答案2
一个更具“创造性的答案”,稍微改变你的分数输入,并假设你总是按照相同的顺序输入每个学生的分数(固定的学生姓名列表),让我得到了这个解决方案:
在输入新子小节的分数时,\scoreN{{4.5,5,2.5,3.3}}
我会在外部文件中写入一行新行,用于存储生成的表格。同时,数据工具表中会生成一行。完成所有子小节区域(日期/小节)后,将使用 Sub Avg 值进行计算,DTLmeanforcolumn
然后再次在生成的选项卡中写入一行,并在第二个数据工具选项卡中写入一行以存储 Suv Avg 值。完成所有日期后,使用 TotalAvg 选项卡写入生成的选项卡的最后一行以计算总平均值。在文档末尾,只需包含生成的外部文件即可\input{tmpFile.tex}
。
当然,代码可以进一步优化,但它似乎可以工作。顺便说一句,我强制为未提交解决方案的学生添加零分,并且我还为这种情况绘制了一张图。这样做的想法是,有些学生提交了解决方案,但得分为零 - 所以我希望在单击表格中的链接时看到解决方案。
\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}
答案3
关于此解决方案
本方案采用Lua实现,因此编译器必须是LuaTeX。
基本上,Lua 中一个
table
名为的对象student_info
会跟踪所有学生信息。例如,如果写:\subsection{2020-07-16} \subsubsection{Simple} \score{A10}{4.5}
那么 的值
student_info["2020-07-16"]["Simple"]["A10"]
就是4.5
。大部分代码负责转换和格式化此表以供输出。表格是用
generate_tex_table()
函数构造的,每次文档编译时都会将构造好的表格保存到 中\jobname.mytable
,可以使用命令来显示表格\printtable
,命令的本质就是\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} }
一组
format_
函数控制数据在输出中的格式。
来源
\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}
桌子
(设计灵感来自木子木志Z的答案。看了他的解决方案,我不禁佩服他的 LaTeX 编程技巧有多好……它很漂亮,他绝对值得这个奖励。我唯一担心的是 LaTeX3 浮点运算的精度。不过,对于这种应用来说,它应该没问题。)
我的想法
以前我非常热衷于 LaTeX 编程,并用 LaTeX 实现了一堆算法。然而,当我的热情开始消退时,我开始思考:“我真的需要在 LaTeX 中实现这个/那个吗“?LaTeX 的问题在于它不是为通用编程而设计的是的,它是图灵完备的,但是在 LaTeX 中实现简单算法的工作量可能会非常巨大。
在您的具体情况下,您尝试直接使用 LaTeX 进行简单的数据处理和文件管理。我尝试使用 Lua 来避免使用 LaTeX3,但事实证明 Lua 也不是实现这些目的的最佳语言:您可能会注意到,我需要编写显式循环块来聚合数组,这在 Python 中是一行代码。事实上,整个Lua代码几乎和LaTeX没有任何关系也就是说,它可以用任何语言来写。
如果我是你,我可能会这么做:
- 将学生信息保存在 CSV 文件中(您可以使用 Microsoft Excel)。
- 使用Python的
pandas
来numpy
处理数据和os.path
管理文件。 - 在 Python 中构建 TeX 源代码。借助 Python 强大的字符串库和派拉泰。
- 将 TeX 源保存到文件并用于
subprocess.run
调用 TeX 可执行文件来编译该文件。
如果效率是关键,我认为这种方法可以节省大量时间,而且与纯 LaTeX/LuaTeX 解决方案相比,结果将更加可定制。尽管如此,我仍然很享受编写上述代码的过程。希望你喜欢我的选择解决方案