We're going to solve an advent of code puzzle in Roc lang. These are the functions you can use: ``` Bool.false : Bool Bool.true : Bool Decode.fromBytesPartial : List U8, fmt -> DecodeResult val where val implements Decoding, fmt implements DecoderFormatting Dict.empty : {} -> Dict * * Dict.fromList : List (k, v) -> Dict k v Dict.get : Dict k v, k -> Result v [KeyNotFound] Dict.insert : Dict k v, k, v -> Dict k v Dict.insertAll : Dict k v, Dict k v -> Dict k v Dict.joinMap : Dict a b, (a, b -> Dict x y) -> Dict x y Dict.keys : Dict k v -> List k Dict.map : Dict k a, (k, a -> b) -> Dict k b Dict.remove : Dict k v, k -> Dict k v Dict.single : k, v -> Dict k v Dict.toList : Dict k v -> List (k, v) Dict.update : Dict k v, k, (Result v [Missing] -> Result v [Missing]) -> Dict k v Dict.walk : Dict k v, state, (state, k, v -> state) -> state Encode.toBytes : val, fmt -> List U8 where val implements Encoding, fmt implements EncoderFormatting List.all : List a, (a -> Bool) -> Bool List.any : List a, (a -> Bool) -> Bool List.append : List a, a -> List a List.chunksOf : List a, U64 -> List (List a) List.concat : List a, List a -> List a List.contains : List a, a -> Bool where a implements Eq List.dropAt : List elem, U64 -> List elem List.dropFirst : List elem, U64 -> List elem List.dropIf : List a, (a -> Bool) -> List a List.dropLast : List elem, U64 -> List elem List.findFirst : List elem, (elem -> Bool) -> Result elem [NotFound] List.findFirstIndex : List elem, (elem -> Bool) -> Result U64 [NotFound] List.findLastIndex : List elem, (elem -> Bool) -> Result U64 [NotFound] List.first : List a -> Result a [ListWasEmpty] List.get : List a, U64 -> Result a [OutOfBounds] List.intersperse : List elem, elem -> List elem List.isEmpty : List * -> Bool List.join : List (List a) -> List a List.joinMap : List a, (a -> List b) -> List b List.keepIf : List a, (a -> Bool) -> List a List.keepOks : List before, (before -> Result after *) -> List after List.last : List a -> Result a [ListWasEmpty] List.len : List * -> U64 List.map : List a, (a -> b) -> List b List.map2 : List a, List b, (a, b -> c) -> List c List.mapTry : List elem, (elem -> Result ok err) -> Result (List ok) err List.mapWithIndex : List a, (a, U64 -> b) -> List b List.max : List (Num a) -> Result (Num a) [ListWasEmpty] List.min : List (Num a) -> Result (Num a) [ListWasEmpty] List.range List.repeat : a, U64 -> List a List.replace : List a, U64, a -> { list : List a, value : a } List.reverse : List a -> List a List.sortAsc : List (Num a) -> List (Num a) List.sortDesc : List (Num a) -> List (Num a) List.sortWith : List a, (a, a -> [LT, EQ, GT]) -> List a List.splitAt : List elem, U64 -> { before : List elem, others : List elem } List.splitOn : List a, a -> List (List a) where a implements Eq List.sublist : List elem, { start : U64, len : U64 } -> List elem List.sum : List (Num a) -> Num a List.takeFirst : List elem, U64 -> List elem List.takeLast : List elem, U64 -> List elem List.update : List a, U64, (a -> a) -> List a List.walk : List elem, state, (state, elem -> state) -> state List.walkBackwards : List elem, state, (state, elem -> state) -> state List.walkTry : List elem, state, (state, elem -> Result state err) -> Result state err List.walkUntil : List elem, state, (state, elem -> [Continue state, Break state]) -> state List.walkWithIndex : List elem, state, (state, elem, U64 -> state) -> state List.walkWithIndexUntil : List elem, state, (state, elem, U64 -> [Continue state, Break state]) -> state List.withCapacity : U64 -> List * Num.abs : Num a -> Num a Num.add : Num a, Num a -> Num a Num.addChecked : Num a, Num a -> Result (Num a) [Overflow] Num.bitwiseAnd : Int a, Int a -> Int a Num.bitwiseOr : Int a, Int a -> Int a Num.ceiling : Frac * -> Int * Num.compare : Num a, Num a -> [LT, EQ, GT] Num.cos : Frac a -> Frac a Num.div : Frac a, Frac a -> Frac a Num.divTrunc : Int a, Int a -> Int a Num.divTruncChecked : Int a, Int a -> Result (Int a) [DivByZero] Num.e : Frac * Num.intCast : Int a -> Int b Num.isApproxEq : Frac a, Frac a, { rtol ? Frac a, atol ? Frac a } -> Bool Num.isEven : Int a -> Bool Num.isMultipleOf : Int a, Int a -> Bool Num.max : Num a, Num a -> Num a Num.maxU64 : U64 Num.min : Num a, Num a -> Num a Num.mul : Num a, Num a -> Num a Num.pow : Frac a, Frac a -> Frac a Num.powInt : Int a, Int a -> Int a Num.rem : Int a, Int a -> Int a Num.round : Frac * -> Int * Num.shiftLeftBy : Int a, U8 -> Int a Num.shiftRightZfBy : Int a, U8 -> Int a Num.sin : Frac a -> Frac a Num.sqrt : Frac a -> Frac a Num.subChecked : Num a, Num a -> Result (Num a) [Overflow] Num.subSaturated : Num a, Num a -> Num a Num.toF64 : Num * -> F64 Num.toFrac : Num * -> Frac * Num.toI64 : Int * -> I64 Num.toI8 : Int * -> I8 Num.toStr : Num * -> Str Num.toU16 : Int * -> U16 Num.toU32 : Int * -> U32 Num.toU64 : Int * -> U64 Num.toU64Checked : Int * -> Result U64 [OutOfBounds] Num.toU8 : Int * -> U8 Result.isOk : Result ok err -> Bool Result.mapErr : Result ok a, (a -> b) -> Result ok b Result.onErr : Result a err, (err -> Result a otherErr) -> Result a otherErr Result.try : Result a err, (a -> Result b err) -> Result b err Result.withDefault : Result ok err, ok -> ok Set.contains : Set k, k -> Bool Set.empty : {} -> Set * Set.fromList : List k -> Set k Set.insert : Set k, k -> Set k Set.intersection : Set k, Set k -> Set k Set.isEmpty : Set * -> Bool Set.keepIf : Set k, (k -> Bool) -> Set k Set.len : Set * -> U64 Set.map : Set a, (a -> b) -> Set b Set.remove : Set k, k -> Set k Set.single : k -> Set k Set.toList : Set k -> List k Set.union : Set k, Set k -> Set k Set.walkUntil : Set k, state, (state, k -> [Continue state, Break state]) -> state Str.contains : Str, Str -> Bool Str.endsWith : Str, Str -> Bool Str.fromUtf8 : List U8 -> Result Str [BadUtf8 Utf8ByteProblem U64] Str.isEmpty : Str -> Bool Str.joinWith : List Str, Str -> Str Str.repeat : Str, U64 -> Str Str.replaceEach : Str, Str, Str -> Str Str.replaceFirst : Str, Str, Str -> Str Str.splitFirst : Str, Str -> Result { before : Str, after : Str } [NotFound] Str.splitOn : Str, Str -> List Str Str.startsWith : Str, Str -> Bool Str.toI16 : Str -> Result I16 [InvalidNumStr] Str.toI64 : Str -> Result I64 [InvalidNumStr] Str.toU32 : Str -> Result U32 [InvalidNumStr] Str.toU64 : Str -> Result U64 [InvalidNumStr] Str.toUtf8 : Str -> List U8 Str.trim : Str -> Str Str.trimEnd : Str -> Str Task.map : Task a c, (a -> b) -> Task b c ``` Some examples of previous advent of code puzzles from 2023: # 1 ## Puzzle --- Day 1: Trebuchet?! --- Something is wrong with global snow production, and you've been selected to take a look. The Elves have even given you a map; on it, they've used stars to mark the top fifty locations that are likely to be having problems. You've been doing this long enough to know that to restore snow operations, you need to check all fifty stars by December 25th. Collect stars by solving puzzles. Two puzzles will be made available on each day in the Advent calendar; the second puzzle is unlocked when you complete the first. Each puzzle grants one star. Good luck! You try to ask why they can't just use a weather machine ("not powerful enough") and where they're even sending you ("the sky") and why your map looks mostly blank ("you sure ask a lot of questions") and hang on did you just say the sky ("of course, where do you think snow comes from") when you realize that the Elves are already loading you into a trebuchet ("please hold still, we need to strap you in"). As they're making the final adjustments, they discover that their calibration document (your puzzle input) has been amended by a very young Elf who was apparently just excited to show off her art skills. Consequently, the Elves are having trouble reading the values on the document. The newly-improved calibration document consists of lines of text; each line originally contained a specific calibration value that the Elves now need to recover. On each line, the calibration value can be found by combining the first digit and the last digit (in that order) to form a single two-digit number. For example: 1abc2 pqr3stu8vwx a1b2c3d4e5f treb7uchet In this example, the calibration values of these four lines are 12, 38, 15, and 77. Adding these together produces 142. Consider your entire calibration document. What is the sum of all of the calibration values? ## Solution ``` app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.17.0/lZFLstMUCUvd5bjnnpYromZJXkQUrdhbva4xdBInicE.tar.br", aoc: "https://github.com/lukewilliamboswell/aoc-template/releases/download/0.2.0/tlS1ZkwSKSB87_3poSOXcwHyySe0WxWOWQbPmp7rxBw.tar.br", } import pf.Stdin import pf.Stdout import pf.Utc import aoc.AoC { stdin: Stdin.readToEnd, stdout: Stdout.write, time: \{} -> Utc.now {} |> Task.map Utc.toMillisSinceEpoch, } main = AoC.solve { year: 2023, day: 1, title: "Trebuchet?!", part1, part2, } exampleInputPart1 = """ 1abc2 pqr3stu8vwx a1b2c3d4e5f treb7uchet """ exampleInputPart2 = """ two1nine eightwothree abcone2threexyz xtwone3four 4nineeightseven2 zoneight234 7pqrstsixteen """ part1 : Str -> Result Str [NotImplemented, Error Str] part1 = \input -> vals = input |> Str.splitOn "\n" |> List.map Str.toUtf8 |> List.map \bytes -> bytes |> List.keepIf isDigit |> List.map toCalibration Ok "The sum of all of the calibration values $(vals |> List.sum |> Num.toStr)" expect part1 exampleInputPart1 == Ok "The sum of all of the calibration values 142" part2 : Str -> Result Str [NotImplemented, Error Str] part2 = \input -> sum = input |> Str.splitOn "\n" |> List.map Str.toUtf8 |> List.map \bytes -> takeDigits { digits: [], rest: bytes } |> List.map toCalibration |> List.sum Ok "The sum of all of the calibration values $(Num.toStr sum)" expect part2 exampleInputPart2 == Ok "The sum of all of the calibration values 281" isDigit : U8 -> Bool isDigit = \b -> b >= '0' && b <= '9' toCalibration : List U8 -> U32 toCalibration = \digits -> when (List.first digits, List.last digits) is (Ok first, Ok last) -> when [first, last] |> Str.fromUtf8 |> Result.try Str.toU32 is Ok n -> n Err _ -> crash "couldnt parse number" _ -> crash "expected at least one number" takeDigits : { digits : List U8, rest : List U8 } -> List U8 takeDigits = \{ digits, rest } -> when rest is [] -> digits [a, ..] if isDigit a -> takeDigits { digits: List.append digits a, rest: List.dropFirst rest 1 } ['o', 'n', 'e', ..] -> takeDigits { digits: List.append digits '1', rest: List.dropFirst rest 1 } ['t', 'w', 'o', ..] -> takeDigits { digits: List.append digits '2', rest: List.dropFirst rest 1 } ['t', 'h', 'r', 'e', 'e', ..] -> takeDigits { digits: List.append digits '3', rest: List.dropFirst rest 1 } ['f', 'o', 'u', 'r', ..] -> takeDigits { digits: List.append digits '4', rest: List.dropFirst rest 1 } ['f', 'i', 'v', 'e', ..] -> takeDigits { digits: List.append digits '5', rest: List.dropFirst rest 1 } ['s', 'i', 'x', ..] -> takeDigits { digits: List.append digits '6', rest: List.dropFirst rest 1 } ['s', 'e', 'v', 'e', 'n', ..] -> takeDigits { digits: List.append digits '7', rest: List.dropFirst rest 1 } ['e', 'i', 'g', 'h', 't', ..] -> takeDigits { digits: List.append digits '8', rest: List.dropFirst rest 1 } ['n', 'i', 'n', 'e', ..] -> takeDigits { digits: List.append digits '9', rest: List.dropFirst rest 1 } _ -> takeDigits { digits, rest: List.dropFirst rest 1 } ``` # 2 ## Puzzle --- Day 2: Cube Conundrum --- You're launched high into the atmosphere! The apex of your trajectory just barely reaches the surface of a large island floating in the sky. You gently land in a fluffy pile of leaves. It's quite cold, but you don't see much snow. An Elf runs over to greet you. The Elf explains that you've arrived at Snow Island and apologizes for the lack of snow. He'll be happy to explain the situation, but it's a bit of a walk, so you have some time. They don't get many visitors up here; would you like to play a game in the meantime? As you walk, the Elf shows you a small bag and some cubes which are either red, green, or blue. Each time you play this game, he will hide a secret number of cubes of each color in the bag, and your goal is to figure out information about the number of cubes. To get information, once a bag has been loaded with cubes, the Elf will reach into the bag, grab a handful of random cubes, show them to you, and then put them back in the bag. He'll do this a few times per game. You play several games and record the information from each game (your puzzle input). Each game is listed with its ID number (like the 11 in Game 11: ...) followed by a semicolon-separated list of subsets of cubes that were revealed from the bag (like 3 red, 5 green, 4 blue). For example, the record of a few games might look like this: Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green In game 1, three sets of cubes are revealed from the bag (and then put back again). The first set is 3 blue cubes and 4 red cubes; the second set is 1 red cube, 2 green cubes, and 6 blue cubes; the third set is only 2 green cubes. The Elf would first like to know which games would have been possible if the bag contained only 12 red cubes, 13 green cubes, and 14 blue cubes? In the example above, games 1, 2, and 5 would have been possible if the bag had been loaded with that configuration. However, game 3 would have been impossible because at one point the Elf showed you 20 red cubes at once; similarly, game 4 would also have been impossible because the Elf showed you 15 blue cubes at once. If you add up the IDs of the games that would have been possible, you get 8. Determine which games would have been possible if the bag had been loaded with only 12 red cubes, 13 green cubes, and 14 blue cubes. What is the sum of the IDs of those games? ## Solution ``` app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.17.0/lZFLstMUCUvd5bjnnpYromZJXkQUrdhbva4xdBInicE.tar.br", aoc: "https://github.com/lukewilliamboswell/aoc-template/releases/download/0.2.0/tlS1ZkwSKSB87_3poSOXcwHyySe0WxWOWQbPmp7rxBw.tar.br", parser: "https://github.com/lukewilliamboswell/roc-parser/releases/download/0.9.0/w8YKp2YAgQt5REYk912HfKAHBjcXsrnvtjI0CBzoAT4.tar.br", } import pf.Stdin import pf.Stdout import pf.Utc import aoc.AoC { stdin: Stdin.readToEnd, stdout: Stdout.write, time: \{} -> Utc.now {} |> Task.map Utc.toMillisSinceEpoch, } import parser.String exposing [string, digits, parseStr, codeunit] import parser.Parser exposing [Parser, map, sepBy, const, keep, skip, oneOf] main = AoC.solve { year: 2023, day: 2, title: "Cube Conundrum", part1, part2, } part1 : Str -> Result Str _ part1 = \input -> games = parseStr? (sepBy parseGame (codeunit '\n')) input ids = List.walkWithIndex games [] \validGames, game, idx -> if isValidGame game then List.append validGames (Num.toU32 idx + 1) else validGames Ok "The sum of the IDs is $(ids |> List.sum |> Num.toStr)" part2 : Str -> Result Str _ part2 = \input -> games = parseStr? (sepBy parseGame (codeunit '\n')) input totalPower = List.walkWithIndex games 0 \sum, game, _ -> { red, green, blue } = calcGameMinCubeSet game power = red * green * blue sum + power Ok "The sum of the power is $(Num.toStr totalPower)" parseNumberColor : Parser (List U8) [Red U32, Green U32, Blue U32] parseNumberColor = const (\number -> \color -> when color is Red -> Red number Green -> Green number Blue -> Blue number ) |> skip (codeunit ' ') |> keep (digits |> map Num.toU32) |> skip (codeunit ' ') |> keep ( oneOf [ string "red" |> map \_ -> Red, string "green" |> map \_ -> Green, string "blue" |> map \_ -> Blue, ] ) expect parseStr parseNumberColor " 64 green" == Ok (Green 64u32) expect parseStr parseNumberColor " 12 red" == Ok (Red 12u32) expect parseStr parseNumberColor " 546 blue" == Ok (Blue 546u32) parseGame : Parser (List U8) (List (List [Red U32, Green U32, Blue U32])) parseGame = const (\i -> i) |> skip (string "Game ") |> skip (digits) |> skip (codeunit ':') |> keep ( parseNumberColor |> sepBy (codeunit ',') |> sepBy (codeunit ';') ) expect parseStr parseGame "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green" == Ok [ [Blue 3u32, Red 4u32], [Red 1u32, Green 2u32, Blue 6u32], [Green 2u32], ] isValidGame : List (List [Red U32, Green U32, Blue U32]) -> Bool isValidGame = \subsets -> when subsets is [] -> Bool.true [subset, ..] -> if isValidSubset subset then isValidGame (List.dropFirst subsets 1) else Bool.false expect isValidGame [[Blue 3u32, Red 4u32], [Red 1u32, Green 2u32, Blue 6u32], [Green 2u32]] expect !(isValidGame [[Blue 15u32]]) isValidSubset : List [Red U32, Green U32, Blue U32] -> Bool isValidSubset = \subsubset -> next = List.dropFirst subsubset 1 when subsubset is [] -> Bool.true [Red count, ..] if count > 12 -> Bool.false [Green count, ..] if count > 13 -> Bool.false [Blue count, ..] if count > 14 -> Bool.false _ -> isValidSubset next expect isValidSubset [Red 1u32, Green 2u32, Blue 6u32] expect !(isValidSubset [Red 100u32, Green 2u32, Blue 6u32]) calcGameMinCubeSet : List (List [Red U32, Green U32, Blue U32]) -> { red : U32, green : U32, blue : U32 } calcGameMinCubeSet = \subsets -> List.walk subsets { red: 0, green: 0, blue: 0 } calcMinCubeSetHelp calcMinCubeSetHelp : { red : U32, green : U32, blue : U32 }, List [Red U32, Green U32, Blue U32] -> { red : U32, green : U32, blue : U32 } calcMinCubeSetHelp = \current, subsubsets -> next = List.dropFirst subsubsets 1 when subsubsets is [] -> current [Red count, ..] if count > current.red -> calcMinCubeSetHelp { current & red: count } next [Green count, ..] if count > current.green -> calcMinCubeSetHelp { current & green: count } next [Blue count, ..] if count > current.blue -> calcMinCubeSetHelp { current & blue: count } next _ -> calcMinCubeSetHelp current next expect game = parseStr parseGame "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green" |> Result.withDefault [] minCubes = calcGameMinCubeSet game minCubes == { red: 4, green: 2, blue: 6 } ``` # 3 ## Puzzle --- Day 3: Gear Ratios --- You and the Elf eventually reach a gondola lift station; he says the gondola lift will take you up to the water source, but this is as far as he can bring you. You go inside. It doesn't take long to find the gondolas, but there seems to be a problem: they're not moving. "Aaah!" You turn around to see a slightly-greasy Elf with a wrench and a look of surprise. "Sorry, I wasn't expecting anyone! The gondola lift isn't working right now; it'll still be a while before I can fix it." You offer to help. The engineer explains that an engine part seems to be missing from the engine, but nobody can figure out which one. If you can add up all the part numbers in the engine schematic, it should be easy to work out which part is missing. The engine schematic (your puzzle input) consists of a visual representation of the engine. There are lots of numbers and symbols you don't really understand, but apparently any number adjacent to a symbol, even diagonally, is a "part number" and should be included in your sum. (Periods (.) do not count as a symbol.) Here is an example engine schematic: 467..114.. ...*...... ..35..633. ......#... 617*...... .....+.58. ..592..... ......755. ...$.*.... .664.598.. In this schematic, two numbers are not part numbers because they are not adjacent to a symbol: 114 (top right) and 58 (middle right). Every other number is adjacent to a symbol and so is a part number; their sum is 4361. Of course, the actual engine schematic is much larger. What is the sum of all of the part numbers in the engine schematic? ## Solution ``` app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.17.0/lZFLstMUCUvd5bjnnpYromZJXkQUrdhbva4xdBInicE.tar.br", aoc: "https://github.com/lukewilliamboswell/aoc-template/releases/download/0.2.0/tlS1ZkwSKSB87_3poSOXcwHyySe0WxWOWQbPmp7rxBw.tar.br", } import pf.Stdin import pf.Stdout import pf.Utc import aoc.AoC { stdin: Stdin.readToEnd, stdout: Stdout.write, time: \{} -> Utc.now {} |> Task.map Utc.toMillisSinceEpoch, } main = AoC.solve { year: 2023, day: 3, title: "Gear Ratios", part1, part2, } exampleInput = """ 467..114.. ...*...... ..35..633. ......#... 617*...... .....+.58. ..592..... ......755. ...$.*.... .664.598.. """ Location : { row : U64, col : U64 } Token : [Digit U8, Dot, Symbol [Asterisk, Slash, Equal, Ampersand, At, Plus, Hash, Minus, Dollar, Percent], Number U64] LocationToken : { loc : Location, token : Token} Schematic : Dict Location Token part1 : Str -> Result Str [NotImplemented, Error Str] part1 = \input -> inputSchematic : Schematic inputSchematic = input |> parseLocationTokens |> filterDots |> toSchematic |> replaceDigitsWithNumbers sum = inputSchematic |> symbolLocations |> List.map (adjacentLocations inputSchematic) |> List.map filterUniqueNumbers |> List.map sumPartNumbers # assume each number can only see 1 symbol, otherwise we will double count |> List.sum Ok "The sum of all of the part numbers in the engine schematic is $(Num.toStr sum)" expect part1 exampleInput == Ok "The sum of all of the part numbers in the engine schematic is 4361" part2 : Str -> Result Str [NotImplemented, Error Str] part2 = \input -> inputSchematic : Schematic inputSchematic = input |> parseLocationTokens |> filterDots |> toSchematic |> replaceDigitsWithNumbers sum = inputSchematic |> gearLocations |> List.map (adjacentLocations inputSchematic) |> List.map filterUniqueNumbers |> List.keepIf \lts -> hasExactlyTwoParts lts [] |> List.map calculateGearRatio |> List.sum Ok "The the sum of all of the gear ratios in the engine schematic is $(Num.toStr sum)" expect part2 exampleInput == Ok "The the sum of all of the gear ratios in the engine schematic is 467835" parseLocationTokens : Str -> List (List LocationToken) parseLocationTokens = \input -> input |> Str.splitOn "\n" |> List.mapWithIndex \rowStr, row -> rowStr |> Str.toUtf8 |> List.mapWithIndex \byte, col -> {loc : {row, col}, token: tokenFromByte byte } tokenFromByte : U8 -> Token tokenFromByte = \b -> when b is a if a >= '0' && a <= '9' -> Digit (a - '0') '*' -> Symbol Asterisk '/' -> Symbol Slash '=' -> Symbol Equal '&' -> Symbol Ampersand '@' -> Symbol At '#' -> Symbol Hash '+' -> Symbol Plus '-' -> Symbol Minus '$' -> Symbol Dollar '%' -> Symbol Percent '.' -> Dot _ -> str = [b] |> Str.fromUtf8 |> Result.withDefault "" crash "token '$(str)' not recognised " expect tokenFromByte '.' == Dot expect tokenFromByte '2' == Digit 2u8 expect tokenFromByte '$' == Symbol Dollar isDot : {token : Token}a -> Bool isDot = \{token} -> token != Dot expect !(isDot {loc: {row: 0, col: 0}, token: Dot}) expect isDot {loc: {row: 0, col: 0}, token: Digit 0} filterDots : List (List LocationToken) -> List LocationToken filterDots = \lts -> lts |> List.join |> List.keepIf isDot toSchematic : List LocationToken -> Schematic toSchematic = \lts -> List.walk lts (Dict.empty {}) \dict, lt -> Dict.insert dict lt.loc lt.token symbolLocations : Schematic -> List Location symbolLocations = \schematic -> Dict.walk schematic [] \acc, loc, token -> when token is Symbol _ -> List.append acc loc _ -> acc # increasing row is down # increasing col is right move : Location -> ([UpLeft, UpRight, DownLeft, DownRight, Left, Up, Right, Down] -> Result Location [Invalid]) move = \{row, col} -> \direction -> maybeRow = when direction is UpLeft -> Num.subChecked row 1 UpRight -> Num.subChecked row 1 DownLeft -> Num.addChecked row 1 DownRight -> Num.addChecked row 1 Left -> Ok row Up -> Num.subChecked row 1 Right -> Ok row Down -> Num.addChecked row 1 maybeCol = when direction is UpLeft -> Num.subChecked col 1 UpRight -> Num.addChecked col 1 DownLeft -> Num.subChecked col 1 DownRight -> Num.addChecked col 1 Left -> Num.subChecked col 1 Up -> Ok col Right -> Num.addChecked col 1 Down -> Ok col when (maybeRow, maybeCol) is (Ok r, Ok c) -> Ok {row: r, col: c} _ -> Err Invalid getToken : Schematic -> (Location -> Result LocationToken [NothingAtLocation]) getToken = \schematic -> \loc -> when Dict.get schematic loc is Ok token -> Ok {loc, token} Err KeyNotFound -> Err NothingAtLocation adjacentLocations : Schematic -> (Location -> List LocationToken) adjacentLocations = \schematic -> \curr -> [UpLeft,UpRight,DownLeft,DownRight,Left,Up,Right,Down] |> List.keepOks (move curr) |> List.keepOks (getToken schematic) # take a schematic and convert any digits into the number at that location replaceDigitsWithNumbers : Schematic -> Schematic replaceDigitsWithNumbers = \schematic -> Dict.map schematic \loc, token -> when token is Digit _ -> Number (getNumberAtLocation schematic loc WalkingLeft) _ -> token # start with a digit, walk left to first digit, then walk right building up the number getNumberAtLocation : Schematic, Location, [WalkingLeft, BuildingNumber U64] -> U64 getNumberAtLocation = \schematic, loc, state -> maybeRight = (move loc) Right |> Result.try \right -> when Dict.get schematic right is Ok (Digit u8) -> Ok (right, u8) _ -> Err Invalid maybeLeft = (move loc) Left |> Result.try \left -> when Dict.get schematic left is Ok (Digit u8) -> Ok (left, u8) _ -> Err Invalid maybeCurrent = when Dict.get schematic loc is Ok (Digit u8) -> Ok (loc, u8) _ -> Err Invalid when (state, maybeLeft, maybeRight, maybeCurrent ) is (WalkingLeft, Ok (left, _), _, Ok _ ) -> getNumberAtLocation schematic left WalkingLeft (WalkingLeft, _, _, Err _ ) -> crash "starting location isn't a digit or isn't in schematic" (WalkingLeft, _, Ok (right, _), Ok (_, u8) ) -> getNumberAtLocation schematic right (BuildingNumber (Num.toU64 u8)) (WalkingLeft, _, _, Ok (_, u8) ) -> Num.toU64 u8 # single number (WalkingLeft, _, _, _ ) -> crash "unable to move right" (BuildingNumber u64, _, Ok (right, _), Ok (_, u8) ) -> getNumberAtLocation schematic right (BuildingNumber ((u64 * 10) + (Num.toU64 u8))) (BuildingNumber u64, _, _, Ok (_, u8) ) -> (u64 * 10) + (Num.toU64 u8) # base case (BuildingNumber _, _, _, _ ) -> crash "moved to a location that isnt a number" # take a list of location/tolen pairs and keep only unique numbers filterUniqueNumbers : List LocationToken -> List LocationToken filterUniqueNumbers = \lts -> filterUniqueNumbersHelp lts [] [] filterUniqueNumbersHelp : List LocationToken, List U64, List LocationToken -> List LocationToken filterUniqueNumbersHelp = \lts, seen, keep -> next = List.dropFirst lts 1 when lts is [] -> keep # base case, keep these locations [first, ..] -> when first.token is Number u64 if !(List.contains seen u64) -> # we haven't seen this number before filterUniqueNumbersHelp next (List.append seen u64) (List.append keep first) _ -> filterUniqueNumbersHelp next seen keep # ignore anything that is not a number sumPartNumbers : List LocationToken -> U64 sumPartNumbers = \lts -> next = List.dropFirst lts 1 when lts is [] -> 0 # base case [first, ..] -> when first.token is Number u64 -> u64 + (sumPartNumbers next) # next _ -> crash "expected only numbers" gearLocations : Schematic -> List Location gearLocations = \schematic -> Dict.walk schematic [] \acc, loc, token -> when token is Symbol Asterisk -> List.append acc loc _ -> acc hasExactlyTwoParts : List LocationToken, List U64 -> Bool hasExactlyTwoParts = \lts, seen -> next = List.dropFirst lts 1 when lts is [] -> List.len seen == 2 [first, .. ] -> when first.token is Number u64 -> hasExactlyTwoParts next (List.append seen u64) _ -> crash "expect only numbers" calculateGearRatio : List LocationToken -> U64 calculateGearRatio = \lts -> expect List.len lts <= 2 next = List.dropFirst lts 1 when lts is [] -> 1 # base case [first, ..] -> when first.token is Number u64 -> u64 * (calculateGearRatio next) # next _ -> crash "expected only numbers" ``` We're now going to solve the first puzzle for advent of code 2024. ## Puzzle --- Day 1: Historian Hysteria --- The Chief Historian is always present for the big Christmas sleigh launch, but nobody has seen him in months! Last anyone heard, he was visiting locations that are historically significant to the North Pole; a group of Senior Historians has asked you to accompany them as they check the places they think he was most likely to visit. As each location is checked, they will mark it on their list with a star. They figure the Chief Historian must be in one of the first fifty places they'll look, so in order to save Christmas, you need to help them get fifty stars on their list before Santa takes off on December 25th. Collect stars by solving puzzles. Two puzzles will be made available on each day in the Advent calendar; the second puzzle is unlocked when you complete the first. Each puzzle grants one star. Good luck! You haven't even left yet and the group of Elvish Senior Historians has already hit a problem: their list of locations to check is currently empty. Eventually, someone decides that the best place to check first would be the Chief Historian's office. Upon pouring into the office, everyone confirms that the Chief Historian is indeed nowhere to be found. Instead, the Elves discover an assortment of notes and lists of historically significant locations! This seems to be the planning the Chief Historian was doing before he left. Perhaps these notes can be used to determine which locations to search? Throughout the Chief's office, the historically significant locations are listed not by name but by a unique number called the location ID. To make sure they don't miss anything, The Historians split into two groups, each searching the office and trying to create their own complete list of location IDs. There's just one problem: by holding the two lists up side by side (your puzzle input), it quickly becomes clear that the lists aren't very similar. Maybe you can help The Historians reconcile their lists? For example: 3 4 4 3 2 5 1 3 3 9 3 3 Maybe the lists are only off by a small amount! To find out, pair up the numbers and measure how far apart they are. Pair up the smallest number in the left list with the smallest number in the right list, then the second-smallest left number with the second-smallest right number, and so on. Within each pair, figure out how far apart the two numbers are; you'll need to add up all of those distances. For example, if you pair up a 3 from the left list with a 7 from the right list, the distance apart is 4; if you pair up a 9 with a 3, the distance apart is 6. In the example list above, the pairs and distances would be as follows: The smallest number in the left list is 1, and the smallest number in the right list is 3. The distance between them is 2. The second-smallest number in the left list is 2, and the second-smallest number in the right list is another 3. The distance between them is 1. The third-smallest number in both lists is 3, so the distance between them is 0. The next numbers to pair up are 3 and 4, a distance of 1. The fifth-smallest numbers in each list are 3 and 5, a distance of 2. Finally, the largest number in the left list is 4, while the largest number in the right list is 9; these are a distance 5 apart. To find the total distance between the left list and the right list, add up the distances between all of the pairs you found. In the example above, this is 2 + 1 + 0 + 1 + 2 + 5, a total distance of 11! Your actual left and right lists contain many location IDs. What is the total distance between your lists? ## Solution ``` app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.17.0/lZFLstMUCUvd5bjnnpYromZJXkQUrdhbva4xdBInicE.tar.br", aoc: "https://github.com/lukewilliamboswell/aoc-template/releases/download/0.2.0/tlS1ZkwSKSB87_3poSOXcwHyySe0WxWOWQbPmp7rxBw.tar.br", } import pf.Stdin import pf.Stdout import pf.Utc import aoc.AoC { stdin: Stdin.readToEnd, stdout: Stdout.write, time: \{} -> Utc.now {} |> Task.map Utc.toMillisSinceEpoch, } main = AoC.solve { year: 2024, day: 1, title: "Historian Hysteria", part1, part2, } ``` Complete the rest of the solution.