···1313function Pandoc(doc)
1414 local new_blocks = {}
1515 local i = 1
1616-1616+1717 while i <= #doc.blocks do
1818 local current = doc.blocks[i]
1919-1919+2020 if current.t == "Para" then
2121- -- Collect all consecutive Haskell code blocks following this paragraph
2121+ -- collect all consecutive haskell code blocks following this paragraph
2222 local code_blocks = {}
2323 local j = i + 1
2424 while j <= #doc.blocks and is_haskell(doc.blocks[j]) do
2525- table.insert(code_blocks, doc.blocks[j])
2525+ local code_block = doc.blocks[j]
2626+2727+ -- Ensure haskell-top blocks have haskell class for syntax highlighting
2828+ local has_haskell_class = false
2929+ for _, class in ipairs(code_block.classes) do
3030+ if class == "haskell" then
3131+ has_haskell_class = true
3232+ break
3333+ end
3434+ end
3535+3636+ if not has_haskell_class then
3737+ -- Add haskell class if not present
3838+ table.insert(code_block.classes, 1, "haskell")
3939+ end
4040+4141+ table.insert(code_blocks, code_block)
2642 j = j + 1
2743 end
2828-4444+2945 if #code_blocks > 0 then
3030- -- Create a code div containing all the code blocks
4646+ -- create a code div containing all the code blocks
3147 local code_div = pandoc.Div(code_blocks, {class = "code"})
3248 local row = pandoc.Div({current, code_div}, {class = "row"})
3349 table.insert(new_blocks, row)
3450 i = j -- skip past all the code blocks we just processed
3551 else
3636- -- No code blocks following, create empty code div
5252+ -- no code blocks following, create empty code div
3753 local empty_div = pandoc.Div({}, {class = "code"})
3854 local row = pandoc.Div({current, empty_div}, {class = "row"})
3955 table.insert(new_blocks, row)
4056 i = i + 1
4157 end
4258 else
4343- -- Not a paragraph, just pass through
5959+ -- not a paragraph, just pass through
4460 table.insert(new_blocks, current)
4561 i = i + 1
4662 end
4763 end
4848-6464+4965 return pandoc.Pandoc(new_blocks, doc.meta)
5066end
+61
src/2025/05.lhs
···11+## Day 5
22+33+As always, we start by parsing the input into something more manageable.
44+55+The input consists of two sections separated by a blank line. The first section contains fresh ingredient ID ranges (e.g., `3-5`), and the second contains available ingredient IDs. We split on `\n\n` to separate these sections, then parse each range into a list of two integers and each point as a single integer:
66+77+```haskell
88+import Data.List.Split (splitOn)
99+1010+parse i = (ranges, points)
1111+ where
1212+ [h1, h2] = splitOn "\n\n" i
1313+ ranges = map (map read . splitOn "-") $ lines h1
1414+ points = map read $ lines h2
1515+```
1616+1717+Our representation of intervals is list with two elements `[start, end]`.
1818+`contains` is a helper function to check if an ingredient ID falls within a given range:
1919+2020+```haskell
2121+contains :: Int -> [Int] -> Bool
2222+contains e [start, end] = e >= start && e <= end
2323+```
2424+2525+### Part 1
2626+2727+For part one, we need to count how many of the available ingredient IDs are fresh. An ID is fresh if it falls within `any` of the fresh ranges:
2828+2929+```haskell
3030+p1 :: [[Int]] -> [Int] -> Int
3131+p1 is = length . filter (\p -> any (contains p) is)
3232+```
3333+3434+### Part 2
3535+3636+For part two, we need to count the total number of unique ingredient IDs covered by all the fresh ranges. The methodology here is to iterate over the intervals sorted by ascending start bounds, while keeping track of the last end-bound we jumped over. This is necessary to avoid double-counting overlapping ranges. If we have `10-14` followed by `12-18`, we should avoid counting `12, 13, 14` twice. So we first account for `10-14`, and keep note of `14`. Upon encountering `12-18`, we then only count `18 - 14 = 4` new points, and not `18 - 12 + 1 = 7` new points, since the three points `12, 13, 14` are accounted for.
3737+3838+```haskell-top
3939+import Data.List (sortBy)
4040+import Data.Ord (comparing)
4141+```
4242+4343+```haskell
4444+p2 :: [[Int]] -> Int
4545+p2 = go 0 0 . sortBy (comparing (!! 0))
4646+ where
4747+ go tot _ [] = tot
4848+ go tot le (i@[start, end] : rest)
4949+ | le > end = go tot le rest
5050+ | contains le i = go (tot + end - le) end rest
5151+ | le < start = go (tot + end - start + 1) end rest
5252+```
5353+5454+Finally, a main function to wrap it all up:
5555+5656+```haskell
5757+main = do
5858+ (is, pts) <- parse <$> getContents
5959+ print $ p1 is pts
6060+ print $ p2 is
6161+```