TeX - LaTeX Asked by I Eat Pork Every Single Day on November 1, 2021
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.
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}
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.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\hlinehline
Total Avg & 2 & 2.5 & 3 & 1.83 & 3.2\hline
end{longtable}
end{landscape}
"Dynamically" means I have to be freed from typing the table above by hands.
How to do so?
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.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\hlinehline
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}
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 = "\makeatlettern\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\makeatothern"
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.)
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:
pandas
, numpy
to process data and os.path
to manage files.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 ?
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}n\hlinen' % 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}
Answered by Alan Xiang on November 1, 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 November 1, 2021
The full example is rather long and I'm sure there are optimize spaces, with only one user command printScoreTable
newly provided.
score
is unchanged.titlesec
is loaded.begin{document}
,end{document}
.score{A01}{...} score{A02}{...}
for one problem, and input score{A02}{...} score{A01}{...}
for another.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,
tex
file making use of those markup command,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}
Answered by muzimuzhi Z on November 1, 2021
Get help from others!
Recent Answers
Recent Questions
© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP