How to split a list of calendar entries by month?

I’m thinking of formatting a yearly calendar of outreach events, starting with a Yaml list similar to this:

"Oolong Day":
  Date: 2024-08-20
  Lead: baptiste 
  Purpose: Sipping

"Steep learning curve":
  Date: 2024-08-28
  Lead: Tea master 
  Purpose: Engagement

"Gong fu cha vs instant powder":
  Date: 2024-09-04
  Lead: All
  Purpose: Inform

I’ve started with a possible layout here:
Screenshot 2024-10-01 at 10.42.08

but I’m not sure how to automatically grab the data corresponding to a given month, and insert it in the corresponding grid. Any tips? Ideally I’d end up with a list similar to:

(1: (<event 1>, <event 2>, ...),
2: (<event 1>, <event 2>, ...),
3: (<event 1>, <event 2>, ...),
...
12: (<event 1>, <event 2>, ...),
)

and I could then just grab the n-th month set of events and insert those in the grid.

I’ve tried:

#let el = yaml("dummy-events.yaml")
("01","02","03","04","05","06","07","08","09","10","11","12").map(month => #el.pairs().filter(it => it.at(1).Date.slice(5,7) == month))

but for some reason it complains that month isn’t known? (scoping issue within filter I guess)

To fix the error you need to put the whole second line in code mode: put a # at the start and remove the # from #el.pair.

1 Like

Thanks! I find this use of # to refer to code mode (but also variables in normal mode) endlessly confusing; somehow I’d made it work by adding a #let variable = ... at the start, which I guess does the same thing as #()....

Yes exactly: when you start writing it’s always in markup mode. When you write the # character it enters code mode until the end of the command / parentheses / etc. So #(...) enters code mode until the closing parenthesis, and #let variable = ... enters code mode until the end of the variable definition.

For input, let’s use this (to make reproducible examples):

// #set page(width: 10cm, height: auto)
#let yaml-string = "
\"Oolong Day\":
  Date: 2024-08-20
  Lead: baptiste
  Purpose: Sipping

\"Steep learning curve\":
  Date: 2024-08-28
  Lead: Tea master
  Purpose: Engagement

\"Gong fu cha vs instant powder\":
  Date: 2024-09-04
  Lead: All
  Purpose: Inform
"
#let dict = yaml.decode(yaml-string)
#dict
Output

image

If you want to use 2-char string like you did here:

#("01","02","03","04","05","06","07","08","09","10","11","12").map(month => dict.pairs().filter(it => it.at(1).Date.slice(5,7) == month))

Then you can do something like this:

#let format-month(n) = ("0" + str(n)).slice(-2)
#let months = range(12).map(i => format-month(i + 1))
#let belongs-to-month(month) = ((k, v)) => v.Date.slice(5, 7) == month

// This returns `array` of pairs per event
// (`array` of `array`/tuple of `str` and `dictionary`).
#let get-related-events(month) = dict.pairs().filter(belongs-to-month(month))
#let per-month-events = months.map(get-related-events)
#per-month-events
Output

This will get you array where each element is array for a given month. Each such element contain events represented by a tuple (array of event name and its data).

If you want to just group initial events into months without changing types:

#let format-month(n) = ("0" + str(n)).slice(-2)
#let belongs-to-month(month) = ((k, v)) => v.Date.slice(5, 7) == month
#let pair-to-dict(pair) = { let (k, v) = pair; ((k): v) }
// This returns `dictionary` per event (`array` of `dictionary`).
#let get-related-events(month) = {
  dict
  .pairs()
  .filter(belongs-to-month(month))
  .map(pair-to-dict)
  .fold((:), (v, acc) => acc + v)
  // let d = dict
  // .pairs()
  // .filter(belongs-to-month(month))
  // .map(pair-to-dict)
  // .join()
  // if d == none { (:) } else { d }
}
#let months = range(12).map(i => format-month(i + 1))
#let per-month-events = months.map(get-related-events)
#per-month-events
Output

image

Here each month is an element of array which itself is a dictionary containing only necessary events.

Looking at this:

(1: (<event 1>, <event 2>, ...),
2: (<event 1>, <event 2>, ...),
3: (<event 1>, <event 2>, ...),
...
12: (<event 1>, <event 2>, ...),
)

This looks like a dictionary of type (int, array). Unfortunately, in Typst, dictionary values are indexed by strings only, so you can’t have int keys. But the closest solution will still be:

#let format-month(n) = ("0" + str(n)).slice(-2)
#let pair-to-dict(pair) = { let (k, v) = pair; ((k): v) }
#let belongs-to-month(month) = ((k, v)) => v.Date.slice(5, 7) == format-month(month)
// This returns `dictionary` per event (`array` of `dictionary`).
#let get-related-events(month) = {
  (str(month): dict
  .pairs()
  .filter(belongs-to-month(month))
  .map(pair-to-dict))
}
#let months = range(1, 13)
#let month-events-dict = months.map(get-related-events).join()
#month-events-dict
Output

But if you want to index by int rather than by str:

#let format-month(n) = ("0" + str(n)).slice(-2)
#let pair-to-dict(pair) = { let (k, v) = pair; ((k): v) }
#let belongs-to-month(month) = ((k, v)) => v.Date.slice(5, 7) == format-month(month)
// This returns `array` of `dictionary`-events per month.
#let get-related-events(month) = {
  dict
  .pairs()
  .filter(belongs-to-month(month))
  .map(pair-to-dict)
}
#let months = range(1, 13)
#let month-events-dict = months.map(get-related-events)
#month-events-dict
Output

If you prefer to have less expressive/maintainable but much smaller code, here is a MWE:

#set page(width: 10cm, height: auto)
#let yaml-string = "
\"Oolong Day\":
  Date: 2024-08-20
  Lead: baptiste
  Purpose: Sipping

\"Steep learning curve\":
  Date: 2024-08-28
  Lead: Tea master
  Purpose: Engagement

\"Gong fu cha vs instant powder\":
  Date: 2024-09-04
  Lead: All
  Purpose: Inform
"
#let dict = yaml.decode(yaml-string)
#let month-events-dict = range(1, 13).map(month =>
  dict
  .pairs()
  .filter(
    ((k, v)) => v.Date.slice(5, 7) == ("0" + str(month)).slice(-2)
  )
  .map(
    ((k, v)) => ((k): v)
  )
)
#month-events-dict
Output

Ultimately, I think I provided enough examples so you can mix and match these solutions to get yet another output.