How to create a function that takes a array of dictionaries?

I want to create a function that takes data for a table. I’d like it to have one parameter that takes a result that is mutiple rows of data:

Name Feedback Status
John Did not find the button Not OK
Jane Did find the button immediately OK

What is the best way to pass this to my function? Note that this data does not come from an external source like a CSV, I plan to put it directly as a function argument in Typst.

Below you find my currently failing attempt at constructing a function like that

#let exercise-table = (
id,
task,
result: ((name: "", feedback: "", status: ""),)
) = {
  table(
    columns: (auto, 1fr, auto),
    [ID], table.cell(colspan: 2, id),
    [Task], table.cell(colspan: 2, task),
    for r in result {
         table.cell[r.at("name")]
         table.cell[r.at("feedback")]
         table.cell[r.at("status")]
    }
  )
}

Calling with

exercise-table(
   "1",
   "Test",
   ((
      name: "John",
      feedback: "Did not find the button",
      status: "Not OK"
   ), )

Apologies for the title, I couldn’t think of anything better.

Passing an array of dictionaries to the function is perfectly fine. Inside the table you can then directly grab the values from the dictionaries to fill the table. This only works under the assumption that every row of the table has all three items “name”, “feedback” and “status”. If this is not the case, you have to add the relevant checks inside result.map().

#let exercise-table(
  id,
  task,
  result: ((name: "", feedback: "", status: ""),)
) = {
  table(
    columns: (auto, 1fr, auto),
    [ID], table.cell(colspan: 2, id),
    [Task], table.cell(colspan: 2, task),
    ..result.map(row => row.values()).flatten()
  )
}

#exercise-table(
   "1",
   "Test",
   result: ((
      name: "John",
      feedback: "Did not find the button",
      status: "Not OK"
   ), )
 )

Since you did not specify which part of your example was failing exactly, these are the two mistakes I noticed:

  • By using table.cell[] with square brackets the function expects content. You either have to use the prefix # to access the variable r or you can directly use parentheses table.cell(r.at("name")).
  • The argument “result” is a named argument. You therefore have to explicitly use the name when calling the function.

Thank you, this worked! I got a wierd error regarding “Module not found” from my template.typ, so something strange was going on. I think my main issue was that I did not specify result: on the function call.

I don’t understand the logic behind additional id and task arguments that create 2 rows of 2 columns in a 3 column table. Is this intended? IMO, this is not readable and not how tables are usually used.

Also, not sure if you actually need to use dictionary over a list. From the for loop, it looks like you just want to create the table provided in the OP. And normally you would put table in a figure with a caption and maybe add a label.

If you really just want to recreate the table, use this:

#let exercise-table(result: ()) = table(
  columns: (auto, 1fr, auto),
  table.header[Name][Feedback][Status],
  ..result.map(dictionary.values).flatten(),
)

#exercise-table(
  result: (
    (name: "John", feedback: "Did not find the button", status: "Not OK"),
    (name: "Jane", feedback: "Did find the button immediately", status: "OK"),
  ),
)
with id & task
#let exercise-table(id, task, result: ()) = table(
  columns: (auto, 1fr, auto),
  [ID],
  table.cell(colspan: 2, id),
  [Task],
  table.cell(colspan: 2, task),
  ..result.map(dictionary.values).flatten(),
)

#exercise-table(
  "1",
  "Test",
  result: (
    (name: "John", feedback: "Did not find the button", status: "Not OK"),
    (name: "Jane", feedback: "Did find the button immediately", status: "OK"),
  ),
)

Or this:

#let exercise-table(result) = table(
  columns: (auto, 1fr, auto),
  table.header[Name][Feedback][Status],
  ..result.map(dictionary.values).flatten(),
)

#exercise-table((
  (name: "John", feedback: "Did not find the button", status: "Not OK"),
  (name: "Jane", feedback: "Did find the button immediately", status: "OK"),
))
with id & task
#let exercise-table(id, task, result) = table(
  columns: (auto, 1fr, auto),
  [ID],
  table.cell(colspan: 2, id),
  [Task],
  table.cell(colspan: 2, task),
  ..result.map(dictionary.values).flatten(),
)

#exercise-table(
  "1",
  "Test",
  (
    (name: "John", feedback: "Did not find the button", status: "Not OK"),
    (name: "Jane", feedback: "Did find the button immediately", status: "OK"),
  ),
)

Or even this:

#let exercise-table(..result) = table(
  columns: (auto, 1fr, auto),
  table.header[Name][Feedback][Status],
  ..result.pos().map(dictionary.values).flatten(),
)

#exercise-table(
  (name: "John", feedback: "Did not find the button", status: "Not OK"),
  (name: "Jane", feedback: "Did find the button immediately", status: "OK"),
)
with id & task
#let exercise-table(id, task, ..result) = table(
  columns: (auto, 1fr, auto),
  [ID],
  table.cell(colspan: 2, id),
  [Task],
  table.cell(colspan: 2, task),
  ..result.pos().map(dictionary.values).flatten(),
)

#exercise-table(
  "1",
  "Test",
  (name: "John", feedback: "Did not find the button", status: "Not OK"),
  (name: "Jane", feedback: "Did find the button immediately", status: "OK"),
)

But without additional context, I don’t see a point in using the dictionary:

// Row schema: (name, feedback, status)
#let exercise-table(..result) = table(
  columns: (auto, 1fr, auto),
  table.header[Name][Feedback][Status],
  ..result.pos().flatten(),
)

#exercise-table(
  ("John", "Did not find the button", "Not OK"),
  ("Jane", "Did find the button immediately", "OK"),
)
with id & task
// Row schema: (name, feedback, status)
#let exercise-table(id, task, ..result) = table(
  columns: (auto, 1fr, auto),
  [ID],
  table.cell(colspan: 2, id),
  [Task],
  table.cell(colspan: 2, task),
  ..result.pos().flatten(),
)

#exercise-table(
  "1",
  "Test",
  ("John", "Did not find the button", "Not OK"),
  ("Jane", "Did find the button immediately", "OK"),
)

The cool thing about dictionaries though is that they preserve order:

You can iterate over the pairs in a dictionary using a for loop. This will iterate in the order the pairs were inserted / declared.

Which is why converting it to flatten array works.

The table is supposed to represent a exercise (described by the Task row) that has an ID (it has some more fields in my document, but I simplified it a little to focus on results). The data passed in with result represents different people solving that task and their feedback on it. The colspan: 2 is just for aesthetic reasons.

I was unsure if I should use a dictionary, but I thought it would be nice to have names assigned to the data.