Yes, there are just two things to make sure:
- you need to store the data somewhere before creating the table
- you need to make sure that you operate on numbers and not
content
For demonstration, let’s look at a table you may be starting with:
#table(
columns: 2,
align: right,
stroke: none,
table.header([Col A], [Col B]),
table.hline(),
[1], [2],
[3], [4],
[5], [6],
table.hline(),
[9],
[12],
)
This table has three rows of actual data, a header, and manually calculated column sums.
Advice 1 means you should create a variable to store the data before putting it into the table:
#let data = (
([1], [2]),
([3], [4]),
([5], [6]),
)
#table(
// ...
table.hline(),
..data.flatten(),
table.hline(),
// ...
)
data
contains an array of arrays – each element is a row, and each row consists of the two columns. This is easier for us to work with, but we need flatten()
because table()
doesn’t accept this kind of nested array.
Advice 2 means that instead of writing [1]
, we should just use 1
, so that we can actually calculate something:
#let data = (
(1, 2),
(3, 4),
(5, 6),
)
#table(
// ...
table.hline(),
..data.flatten().map(num => [#num]),
table.hline(),
// ...
)
Again, table()
can’t simply show numbers, thus I also used map(num => [#num])
to convert each number back into content before displaying.
But by doing this, we’re now ready to actually do calculations with the numbers:
// ...
#let col-a-sum = data.map(row => row.at(0)).sum()
#let col-b-sum = data.map(row => row.at(1)).sum()
#table(
// ...
table.hline(),
..data.flatten().map(num => [#num]),
table.hline(),
[#col-a-sum], [#col-b-sum],
)
This extracts from each row the first or second column, respectively, and sums them up. Then, the numbers are added to the table as content.
In this example, all columns were summed up. To avoid duplication, you could do all columns in a loop like this:
#let col-sums = ()
#for i in array.range(data.first().len()) {
let col-sum = data.map(row => row.at(i)).sum()
col-sums.push(col-sum)
}
#table(
// ...
..col-sums.map(num => [#num]),
)
This can be shortened in a few ways; I wanted to show a loop as the main solution because it’s not as terse as these other options, but they do exist:
Shorter sums for all columns
// like the loop before, but using `map()`
// instead of pushing element by element
#let col-sums = array.range(data.first().len()).map(i => {
data.map(row => row.at(i)).sum()
})
// using a sort of transpose operation to get
// an array of columns from the array of rows
#let col-sums = array.zip(..data).map(array.sum)
As a final example, here’s how to sum up all columns read from a csv file with header:
Table with column sums from CSV
#let csv-data = csv.decode(```
Col A,Col B
1,2
3,4
5,6
```.text)
// When the data come from a file, you'd write this instead:
// #let csv-data = csv("filename.csv")
#let (header, ..data) = csv-data
#let col-sums = array.zip(..data).map(column => column.map(int).sum())
#table(
columns: 2,
align: right,
stroke: none,
table.header(..header),
table.hline(),
..data.flatten(),
table.hline(),
..col-sums.map(num => [#num]),
)
What’s a bit different here is that instead of having to convert the data to content
, it comes out of the csv file as strings. So we don’t need to convert to content (strings work in tables), but do need to convert each number to int
before calculating.