···11function is_haskell(block)
22- return block and block.t == "CodeBlock" and block.classes[1] == "haskell"
22+ if not block or block.t ~= "CodeBlock" then
33+ return false
44+ end
55+ for _, class in ipairs(block.classes) do
66+ if class == "haskell" or class == "haskell-top" or class:match("^haskell") then
77+ return true
88+ end
99+ end
1010+ return false
311end
412513function Pandoc(doc)
614 local new_blocks = {}
715 local i = 1
88-1616+917 while i <= #doc.blocks do
1018 local current = doc.blocks[i]
1111- local next_block = doc.blocks[i + 1]
1212-1313- if current.t == "Para" and is_haskell(next_block) then
1414- local code_div = pandoc.Div({next_block}, {class = "code"})
1515- local row = pandoc.Div({current, code_div}, {class = "row"})
1616-1717- table.insert(new_blocks, row)
1818- i = i + 2 -- skip the next block
1919- elseif current.t == "Para" and not is_haskell(next_block) then
2020- local empty_div = pandoc.Div({}, {class = "code"})
2121- local row = pandoc.Div({current, empty_div}, {class = "row"})
2222- table.insert(new_blocks, row)
2323- i = i + 1
1919+2020+ if current.t == "Para" then
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])
2626+ j = j + 1
2727+ end
2828+2929+ if #code_blocks > 0 then
3030+ -- Create a code div containing all the code blocks
3131+ local code_div = pandoc.Div(code_blocks, {class = "code"})
3232+ local row = pandoc.Div({current, code_div}, {class = "row"})
3333+ table.insert(new_blocks, row)
3434+ i = j -- skip past all the code blocks we just processed
3535+ else
3636+ -- No code blocks following, create empty code div
3737+ local empty_div = pandoc.Div({}, {class = "code"})
3838+ local row = pandoc.Div({current, empty_div}, {class = "row"})
3939+ table.insert(new_blocks, row)
4040+ i = i + 1
4141+ end
2442 else
4343+ -- Not a paragraph, just pass through
2544 table.insert(new_blocks, current)
2645 i = i + 1
2746 end
2847 end
2929-4848+3049 return pandoc.Pandoc(new_blocks, doc.meta)
3150end
···11-= 2016
11+# 2016
2233-== Day 1
33+## Day 1
4455-=== Part 1
55+### Part 1
6677We start with some imports obviously:
8899-\begin{code}
99+```haskell
1010{-# LANGUAGE LambdaCase #-}
1111import qualified Data.Set as S
1212-\end{code}
1212+```
13131414Once we have those in place, begin by parsing the input; the input is of the form:
1515···19192020Where `C` is a letter, `NN` form a natural number. First we normalize the input by appending a "," to the end. We then use `words` to split on spaces, now each element is of the form `CNN,`. Using `init` we can drop the trailing comma, and finally, call `extract` to parse `C` and `NN` separately.
21212222-\begin{code}
2222+```haskell
2323parse :: String -> [(Char, Int)]
2424parse input = map (extract . init) $ words normalized
2525 where normalized = input ++ ","
2626 extract (d:rest) = (d, read rest)
2727-\end{code}
2727+```
28282929Next, we define a list of axes, the +x axis is defined as (1, 0) and so on:
30303131-\begin{code}
3131+```haskell
3232axis :: [(Int, Int)]
3333axis = [(0, 1), (1, 0), (0, -1), (-1, 0)]
3434-\end{code}
3434+```
35353636And finally, the first bit of interesting computation; given a direction, we apply a move to the right or left. Each face is represented by an integer (0 being North, 1 being East etc.),
3737Turning right adds one; and turing left subtracts one, the modulo makes the computation cyclic:
38383939-\begin{code}
3939+```haskell
4040dir :: Int -> Char -> Int
4141dir face = \case
4242 'R' -> (face + 1) `mod` 4
4343 'L' -> (face - 1) `mod` 4
4444-\end{code}
4444+```
45454646Now, given a position, and a movement; we can compute the new position like so; first compute the new face to look at by turning,
4747next, determine the axis that we would move along. If we are moving along the +x axis, (dx, dy) is set to (1, 0). Thus the new position is given by movind `l * dx` along the x-axis, and `l * dy` along the y-axis (which would be zero if `dy` is zero).
48484949-\begin{code}
4949+```haskell
5050move (face, x, y) (d, l) = (nf, x + l * dx, y + l * dy)
5151 where nf = dir face d
5252 (dx, dy) = axis !! nf
5353-\end{code}
5353+```
54545555The problem requires us to compute the Manhattan distance of the destination location; which is given by:
56565757-\begin{code}
5757+```haskell
5858mag (_, x, y) = abs x + abs y
5959-\end{code}
5959+```
60606161Thus, the solution to part 1 is given by `p1`; apply the `move` function starting at (0, 0, 0); and applying all the moves in the input. Finally compute the Manhattan distance of the destination from the origin.
62626363-\begin{code}
6363+```haskell
6464p1 :: [(Char, Int)] -> Int
6565p1 n = mag $ foldl' move (0, 0, 0) n
6666-\end{code}
6666+```
67676868-=== Part 2
6868+### Part 2
69697070In the second part, we are tasked with finding the first point that we cross over. This requires us to first enumerate all the points we have visited by making each move.
71717272This `range` function is a helper to enumerate all points between two integers:
73737474-\begin{code}
7474+```haskell
7575range :: Int -> Int -> [Int]
7676range a b
7777 | a <= b = [a .. b]
7878 | otherwise = [a, a - 1 .. b]
7979-\end{code}
7979+```
80808181The first point we cross over will then be the first duplicate element in the list of points we visit, `firstDup` uses `Data.Set` to determine the first duplicate point from a sequence:
82828383-\begin{code}
8383+```haskell
8484firstDup :: Ord a => [a] -> a
8585firstDup xs = go S.empty xs
8686 where
8787 go seen (x : xs)
8888 | x `S.member` seen = x
8989 | otherwise = go (S.insert x seen) xs
9090-\end{code}
9090+```
91919292Thus, the solution to the second part is given by `p2`. First, `pts` is calculated as all the final locations after each move. We then run through these pairwise and calculate all the points in between. Finally, we determine the magnitude of the first point we visit twice.
93939494-\begin{code}
9494+```haskell
9595p2 :: [(Char, Int)] -> Int
9696p2 n = mag $ firstDup coords
9797 where
···104104 y <- range y1 y2,
105105 (x, y) /= (x1, y1)
106106 ]
107107-\end{code}
107107+```
108108109109Finally the `main` function is defined like so:
110110111111-\begin{code}
111111+```haskell
112112input = "R8, R4, R4, R8"
113113main = do
114114 let f = parse input
115115 print $ p1 f
116116 print $ p2 f
117117-\end{code}
117117+```
+4-4
src/2016/02.lhs
···11-== Day 2
11+## Day 2
2233-=== Part 1
33+### Part 1
4455-\begin{code}
55+```haskell
66{-# LANGUAGE LambdaCase #-}
7788import qualified Data.Map as M
···4343 let f = lines n
4444 print $ p1 f
4545 print $ p2 f
4646-\end{code}
4646+```
+70
src/2025/02.lhs
···11+# 2025
22+33+## Day 2
44+55+Starting with the parse step, the input is of the form `11-22,15-17,...`.
66+So we first split on commas,
77+for each item in the list (of the form `"11-22"`), we can split again on `"-"`,
88+and finally, use `read` to parse them to integers.
99+1010+```haskell
1111+import Data.List.Split (splitOn)
1212+1313+parse :: String -> [[Int]]
1414+parse = map (map read . splitOn "-") . splitOn ","
1515+```
1616+1717+We can move onto the problem itself now.
1818+1919+### Part 1
2020+2121+The check for invalidity for an integer in Part 1 requires us to simply split the string in half and ensure that the two halves are identical, this is easily produced using `splitAt`:
2222+2323+```haskell
2424+invalid :: Int -> Bool
2525+invalid n = l == r
2626+ where
2727+ s = show n
2828+ (l, r) = splitAt (length s `div` 2) s
2929+```
3030+3131+With that in place, we can define the solution to part 1. `range` is a utility to iterate through the pair, we then filter out `invalid` items and add everything up:
3232+3333+```haskell
3434+range [a, b] = [a .. b]
3535+3636+p1 i = sum $ concatMap (filter invalid . range) i
3737+```
3838+3939+### Part 2
4040+4141+This part requires a slight modification to the invalidity check. We now need to check for any number of repeating substrings, so "123123123" is invalid because it is formed from `"123" + "123" + "123"`.
4242+The methodology here is slightly involved, first we get all prefixes of the input using `inits`.
4343+We can filter out the initial empty prefix with `tail`. Upon repeated cyling the prefixes, if we are able to form the original string, then the string can be considered invalid!
4444+4545+```haskell-top
4646+import Data.List (inits, isPrefixOf)
4747+```
4848+4949+```haskell
5050+invalid2 :: Int -> Bool
5151+invalid2 n = any ((s `isPrefixOf`) . cycle) $ tail $ inits l
5252+ where
5353+ s = show n
5454+ (l, _) = splitAt (length s `div` 2) s
5555+```
5656+5757+Then, part 2 is defined like so:
5858+5959+```haskell
6060+p2 i = sum $ concatMap (filter invalid2 . range) i
6161+```
6262+6363+Finally, a main function to wrap it all up:
6464+6565+```haskell
6666+main = do
6767+ n <- parse <$> getContents
6868+ print $ p1 n
6969+ print $ p2 n
7070+```
+26
src/2025/03.hs
···11+import Data.Char (digitToInt)
22+import Data.List (tails)
33+44+joltage _ [] = []
55+joltage 0 _ = []
66+joltage need bank
77+ | length bank == need = bank
88+ | otherwise =
99+ let size = length bank - need + 1
1010+ lead = maximum $ take size bank
1111+ after = tail $ dropWhile (/= lead) bank
1212+ in lead : joltage (need - 1) after
1313+1414+toInt = foldl ((+) . (* 10)) 0
1515+1616+p1 n = sum $ map (toInt . joltage 2) n
1717+1818+p2 n = sum $ map (toInt . joltage 12) n
1919+2020+parse :: String -> [[Int]]
2121+parse = map (map digitToInt) . lines
2222+2323+main = do
2424+ n <- parse <$> getContents
2525+ print $ p1 n
2626+ print $ p2 n
+81
src/2025/04.lhs
···11+## Day 4
22+33+As always, we start by parsing the input into something more manageable:
44+55+```haskell
66+import Data.Set
77+88+parse :: String -> Set (Int, Int)
99+parse = grid . lines
1010+```
1111+1212+`grid` is a helper function that produces a `Set` of points. An interesting note about this problem is that the points that do *not* contain rolls (points that are not `@`) are inconsequential to the problem itself, and we can fully ignore them. Using a list comprehension, we first enumerate each line of the input, and then enumerate each character of that line, and only preserve the characters that are `@`:
1313+1414+```haskell
1515+grid l =
1616+ fromList
1717+ [ (x, y)
1818+ | (r, x) <- zip l [0 ..],
1919+ (c, y) <- zip r [0 ..],
2020+ c == '@'
2121+ ]
2222+```
2323+2424+Now that are input has been munged, lets move onto the problem itself.
2525+2626+### Part 1
2727+2828+This part requires us to first calculate all points adjacent to a roll, if there are more than 4 rolls at these adjacent locations, disqualify the roll, as it cannot be lifted by a forklift. First, lets generate a list comprehension that produces the points adjacent to a given point. We ignore the offset (0, 0) because that is the point itself:
2929+3030+```haskell
3131+adjs (x, y) =
3232+ [ (x + x', y + y')
3333+ | x' <- [-1, 0, 1],
3434+ y' <- [-1, 0, 1],
3535+ (x', y') /= (0, 0)
3636+ ]
3737+```
3838+3939+Then, a roll is liftable by a forklift if the adjacent points only have upto 4 rolls. We can use `Set.filter` to traverse the entire grid. Our filter predicate first collects adjacent points, filters out ones that are within the boundaries of the grid, and counts them. Remember that our grid does not contain points that are *not* rolls anyway!
4040+4141+```haskell
4242+liftable g = Data.Set.filter pred g
4343+ where
4444+ pred =
4545+ (< 4)
4646+ . length
4747+ . Prelude.filter (`member` g)
4848+ . adjs
4949+```
5050+5151+Thus, the solution to part one is given as:
5252+5353+```haskell
5454+p1 = size . liftable
5555+```
5656+5757+### Part 2
5858+5959+This part is very similar to the previous part. We need to sequentially remove liftable rolls until we can lift no more. A simple recursive function can do the trick. The base case: if no more rolls can be lifted from the grid, return the number of rolls lifted thus far. The recursive case: accumulate the rolls that are liftable in the current state of the grid, and recurse into the grid where the rolls are removed! The removal is calculated simply using `Set.\\`, which performs set difference:
6060+6161+```haskell
6262+go s g
6363+ | size lifted == 0 = s
6464+ | otherwise = go (s + size lifted) (g \\ lifted)
6565+ where
6666+ lifted = liftable g
6767+```
6868+6969+Thus, the solution for the second part is given like so:
7070+7171+```haskell
7272+p2 = go 0
7373+```
7474+Finally, a main function to wrap it all up:
7575+7676+```haskell
7777+main = do
7878+ n <- parse <$> getContents
7979+ print $ p1 n
8080+ print $ p2 n
8181+```
+212
unlit/unlit.hs
···11+-- Copyright (c) 2012-2025 Simon Hengel <sol@typeful.net>
22+-- Copyright (c) 2025 Akshay Oppiliappan <me@oppi.li>
33+--
44+-- Permission is hereby granted, free of charge, to any person obtaining a copy
55+-- of this software and associated documentation files (the "Software"), to deal
66+-- in the Software without restriction, including without limitation the rights
77+-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
88+-- copies of the Software, and to permit persons to whom the Software is
99+-- furnished to do so, subject to the following conditions:
1010+--
1111+-- The above copyright notice and this permission notice shall be included in
1212+-- all copies or substantial portions of the Software.
1313+--
1414+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1515+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1616+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1717+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1818+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1919+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2020+-- THE SOFTWARE.
2121+{-# LANGUAGE CPP #-}
2222+{-# LANGUAGE DerivingStrategies #-}
2323+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2424+{-# LANGUAGE LambdaCase #-}
2525+{-# LANGUAGE OverloadedStrings #-}
2626+{-# LANGUAGE ViewPatterns #-}
2727+2828+module Main where
2929+3030+import Control.Arrow
3131+import Data.Char
3232+import Data.List (isPrefixOf, sortOn)
3333+import Data.Maybe
3434+import Data.String
3535+import Debug.Trace
3636+import System.Environment
3737+import System.Exit
3838+import System.IO
3939+import Text.Read
4040+4141+fenceChars :: [Char]
4242+fenceChars = ['`', '~']
4343+4444+fences :: [String]
4545+fences = map (replicate 3) fenceChars
4646+4747+main = do
4848+ args <- getArgs
4949+ run (trace (show args) args)
5050+5151+-- | Program entry point.
5252+run :: [String] -> IO ()
5353+run args =
5454+ -- GHC calls unlit like so:
5555+ --
5656+ -- > unlit [args] -h label Foo.lhs /tmp/somefile
5757+ --
5858+ -- [args] are custom arguments provided with -optL
5959+ --
6060+ -- The label is meant to be used in line pragmas, like so:
6161+ --
6262+ -- #line 1 "label"
6363+ --
6464+ case break (== "-h") args of
6565+ (mkSelector -> selector, "-h" : files) -> case files of
6666+ [src, cur, dst] -> do
6767+ readFileUtf8 cur >>= writeFileUtf8 dst . unlit src selector
6868+ [src] -> do
6969+ readFileUtf8 src >>= writeUtf8 stdout . unlit src selector
7070+ _ -> usage
7171+ _ -> usage
7272+ where
7373+ usage :: IO ()
7474+ usage = do
7575+ name <- getProgName
7676+ hPutStrLn stderr ("usage: " ++ name ++ " [selector] -h SRC CUR DST")
7777+ exitFailure
7878+7979+ mkSelector :: [String] -> Selector
8080+ mkSelector = fromMaybe ("haskell" :&: Not "ignore") . parseSelector . unwords
8181+8282+ readFileUtf8 :: FilePath -> IO String
8383+ readFileUtf8 name = openFile name ReadMode >>= \handle -> hSetEncoding handle utf8 >> hGetContents handle
8484+8585+ writeFileUtf8 :: FilePath -> String -> IO ()
8686+ writeFileUtf8 name str = withFile name WriteMode $ \handle -> writeUtf8 handle str
8787+8888+ writeUtf8 :: Handle -> String -> IO ()
8989+ writeUtf8 handle str = hSetEncoding handle utf8 >> hPutStr handle str
9090+9191+unlit :: FilePath -> Selector -> String -> String
9292+unlit src selector = unlines . concatMap formatCodeBlock . sortCodeBlocks . parse
9393+ where
9494+ formatCodeBlock :: CodeBlock -> [String]
9595+ formatCodeBlock cb = ("#line " ++ show (codeBlockStartLine cb) ++ " " ++ show src) : codeBlockContent cb
9696+9797+ sortCodeBlocks :: [CodeBlock] -> [CodeBlock]
9898+ sortCodeBlocks = map fst . sortOn snd . addSortKey
9999+ where
100100+ addSortKey :: [CodeBlock] -> [(CodeBlock, (ReorderingKey, DeclarationOrder))]
101101+ addSortKey = zipWith ((id &&&) . sortKey) [0 ..]
102102+103103+ sortKey :: a -> CodeBlock -> (ReorderingKey, a)
104104+ sortKey n code = (reorderingKey code, n)
105105+106106+ toPredicate :: Selector -> [String] -> Bool
107107+ toPredicate = go
108108+ where
109109+ go s = case s of
110110+ Class c -> elem c
111111+ Not p -> not . go p
112112+ a :&: b -> (&&) <$> go a <*> go b
113113+ a :|: b -> (||) <$> go a <*> go b
114114+115115+newtype DeclarationOrder = DeclarationOrder Int
116116+ deriving newtype (Eq, Ord, Enum, Num)
117117+118118+newtype ReorderingKey = ReorderingKey Int
119119+ deriving newtype (Eq, Show, Read, Ord, Bounded, Num)
120120+121121+reorderingKey :: CodeBlock -> ReorderingKey
122122+reorderingKey = parseReorderingKey . codeBlockClasses
123123+124124+parseReorderingKey :: [String] -> ReorderingKey
125125+parseReorderingKey = go
126126+ where
127127+ go :: [String] -> ReorderingKey
128128+ go = \case
129129+ [] -> 0
130130+ "top" : _ -> minBound
131131+ "haskell-top" : _ -> minBound
132132+ ('t' : 'o' : 'p' : ':' : (readMaybe -> Just n)) : _ -> minBound + n
133133+ ('h' : 'a' : 's' : 'k' : 'e' : 'l' : 'l' : '-' : 't' : 'o' : 'p' : ':' : (readMaybe -> Just n)) : _ -> minBound + n
134134+ _ : classes -> go classes
135135+136136+infixr 3 :&:
137137+138138+infixr 2 :|:
139139+140140+data Selector
141141+ = Class String
142142+ | Not Selector
143143+ | Selector :&: Selector
144144+ | Selector :|: Selector
145145+ deriving (Eq, Show)
146146+147147+parseSelector :: String -> Maybe Selector
148148+parseSelector input = case words input of
149149+ [] -> Nothing
150150+ xs -> (Just . foldr1 (:|:) . map parseAnds) xs
151151+ where
152152+ parseAnds = foldr1 (:&:) . map parseClass . split (== '+')
153153+154154+ parseClass c = case c of
155155+ '!' : xs -> Not (Class xs)
156156+ _ -> Class c
157157+158158+ -- a copy from https://github.com/sol/string
159159+ split :: (Char -> Bool) -> String -> [String]
160160+ split p = go
161161+ where
162162+ go xs = case break p xs of
163163+ (ys, []) -> [ys]
164164+ (ys, _ : zs) -> ys : go zs
165165+166166+instance IsString Selector where
167167+ fromString = Class
168168+169169+data CodeBlock = CodeBlock
170170+ { codeBlockClasses :: [String],
171171+ codeBlockContent :: [String],
172172+ codeBlockStartLine :: Int
173173+ }
174174+ deriving (Eq, Show)
175175+176176+type Line = (Int, String)
177177+178178+parse :: String -> [CodeBlock]
179179+parse = go . zip [2 ..] . lines
180180+ where
181181+ go :: [Line] -> [CodeBlock]
182182+ go xs = case break isFence xs of
183183+ (_, []) -> []
184184+ (_, y : ys) -> case takeCB y ys of
185185+ (cb, rest) -> cb : go rest
186186+187187+ takeCB :: Line -> [Line] -> (CodeBlock, [Line])
188188+ takeCB (n, fence) xs =
189189+ let indent = length . takeWhile isSpace $ fence
190190+ in case break isFence xs of
191191+ (cb, rest) -> (CodeBlock (parseClasses fence) (map (drop indent . snd) cb) n, drop 1 rest)
192192+193193+ isFence :: Line -> Bool
194194+ isFence = p . dropWhile isSpace . snd
195195+ where
196196+ p :: String -> Bool
197197+ p line = any (`isPrefixOf` line) fences
198198+199199+parseClasses :: String -> [String]
200200+parseClasses xs = words $ case dropWhile isSpace . dropWhile (`elem` fenceChars) . dropWhile isSpace $ xs of
201201+ '{' : ys -> map dotToSpace . takeWhile (/= '}') $ ys
202202+ ys -> map dotToSpace . takeWhile (not . isSpace) $ ys
203203+ where
204204+ dotToSpace '.' = ' '
205205+ dotToSpace c = c
206206+207207+replace :: Char -> Char -> String -> String
208208+replace x sub = map f
209209+ where
210210+ f y
211211+ | x == y = sub
212212+ | otherwise = y