christhekeele
Advent of Code 2023 - Day 2
Thought I’d kick today’s thread off!
Parsing
Enum rocks, so most of my code was actually in parsing input.
Preprocessing input
Data model is:
%{
id :: integer() => [
pull :: %{
red: integer(),
green: integer(),
blue: integer()
}
]
}
I modeled each pull as a struct rather than a bare map, just so I didn’t have to write any extra code to hydrate my maps with default 0 values where input was empty for a color.
Source available here.
defmodule AoC.Day.Two.Input do
defmodule Pull do
defstruct red: 0, green: 0, blue: 0
end
def parse(input_file \\ System.fetch_env!("INPUT_FILE")) do
input_file
|> File.read!()
|> String.split("\n")
|> Enum.reject(&(&1 == ""))
|> Enum.map(&parse_game/1)
|> Map.new()
end
def parse_game("Game " <> game) do
{id, rest} = Integer.parse(game)
<<": ">> <> pulls = rest
pulls =
pulls
|> String.trim()
|> String.split(";")
|> Enum.map(&String.trim/1)
|> Enum.map(&parse_pull/1)
{id, pulls}
end
def parse_pull(pull) do
result =
pull
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.map(&parse_pull_color/1)
struct!(Pull, result)
end
def parse_pull_color(result) do
case Integer.parse(result) do
{num, " red"} -> {:red, num}
{num, " green"} -> {:green, num}
{num, " blue"} -> {:blue, num}
end
end
end
Part 1
Solution
input |> Enum.filter(Enum.all?) |> Enum.map |> Enum.sum. Source available here.
defmodule AoC.Day.Two.Part.One do
@red_limit 12
@green_limit 13
@blue_limit 14
def solve(input) do
input
|> Enum.filter(fn {_id, pulls} ->
Enum.all?(pulls, fn
%{red: red, green: green, blue: blue}
when red <= @red_limit and green <= @green_limit and blue <= @blue_limit ->
true
_ ->
false
end)
end)
|> Enum.map(fn {id, _} -> id end)
|> Enum.sum()
end
end
Part 2
Solution
Simpler still. Could have avoided iterating over pulls 3 times, but not too fussed about it. Source available here.
defmodule AoC.Day.Two.Part.Two do
def solve(input) do
input
|> Enum.map(fn {id, pulls} ->
min_red = pulls |> Enum.map(&Map.fetch!(&1, :red)) |> Enum.max()
min_green = pulls |> Enum.map(&Map.fetch!(&1, :green)) |> Enum.max()
min_blue = pulls |> Enum.map(&Map.fetch!(&1, :blue)) |> Enum.max()
{id, min_red * min_green * min_blue}
end)
|> Enum.map(fn {_id, power} -> power end)
|> Enum.sum()
end
end
Most Liked
adamu
After seeing part 2, we know that both parts only need the aggregate of all the rounds, so for each game I used a single map representing the maximum number of cubes seen.
|> Enum.reduce(
%{"red" => 0, "green" => 0, "blue" => 0},
&Map.merge(&1, &2, fn _colour, count1, count2 -> max(count1, count2) end
)
awerment
Just wanted to say I love the use of Integer.parse/1 here!
stevensonmt
My code here. The only notable thing was I decided to force myself to parse manually doing nothing but pattern matching on binaries. Painful and pointless, but also was kind of a fun challenge. Only worked because I knew the limits of the input size. It would fail if any round included any cubes of more than 99 or if there were more than 100 games. Since I knew neither of those conditions applied it was fine.
Here’s the parsing:
def parse(input) do
input
|> Day2.input()
|> Input.lines()
|> Enum.map(&parse_game/1)
|> Enum.map(fn map ->
[k] = Map.keys(map)
v =
Map.values(map)
|> hd()
|> un_nest()
{k, v}
end)
|> Enum.into(%{})
end
defp parse_game(<<"Game ", rest::binary>>), do: parse_game(rest)
defp parse_game(<<i, j, ": ", rest::binary>>) when i in 49..57 and j in 48..57 do
k = (i - 48) * 10 + (j - 48)
Map.put(%{}, k, parse_game(rest))
end
defp parse_game(<<i, ": ", rest::binary>>) when i in 49..57,
do: %{(i - 48) => parse_game(rest)}
defp parse_game(<<"100: ", rest::binary>>), do: %{100 => parse_game(rest)}
defp parse_game(<<i, " blue", rest::binary>>) when i in 49..57 do
[{"blue", i - 48} | parse_game(rest)]
end
defp parse_game(<<i, " red", rest::binary>>) when i in 49..57 do
[{"red", i - 48} | parse_game(rest)]
end
defp parse_game(<<i, " green", rest::binary>>) when i in 49..57 do
[{"green", i - 48} | parse_game(rest)]
end
defp parse_game(<<i, j, " blue", rest::binary>>) when i in 49..57 and j in 48..57 do
[{"blue", (i - 48) * 10 + (j - 48)} | parse_game(rest)]
end
defp parse_game(<<i, j, " red", rest::binary>>) when i in 49..57 and j in 48..57 do
[{"red", (i - 48) * 10 + (j - 48)} | parse_game(rest)]
end
defp parse_game(<<i, j, " green", rest::binary>>) when i in 49..57 and j in 48..57 do
[{"green", (i - 48) * 10 + (j - 48)} | parse_game(rest)]
end
defp parse_game(<<", ", rest::binary>>), do: parse_game(rest)
defp parse_game(<<"; ", rest::binary>>), do: [parse_game(rest)]
defp parse_game(""), do: []
defp un_nest(nested, m \\ %{})
defp un_nest([], m), do: [m]
defp un_nest([hd], m) when is_list(hd),
do: un_nest(hd) ++ [m]
defp un_nest([{k, v} | tl], m), do: un_nest(tl, Map.put(m, k, v))
kip
I also elected in part 1 to build a map that kept the maxima for each color for each game. Which meant that part 2 was really trivial. This kind of “one off” multi-level parsing is a bit tricky to produce easily readable code and @christhekeele definitely did better than I did.
Pre-processing
def parse_input() do
@input
|> String.split("\n", trim: true)
|> Enum.map(fn
<<"Game ", game::binary-3, ": ", pulls::binary>> ->
%{game: String.to_integer(game), max: extract_game_max(pulls)}
<<"Game ", game::binary-2, ": ", pulls::binary>> ->
%{game: String.to_integer(game), max: extract_game_max(pulls)}
<<"Game ", game::binary-1, ": ", pulls::binary>> ->
%{game: String.to_integer(game), max: extract_game_max(pulls)}
end)
end
@default_pull_max %{red: 0, green: 0, blue: 0}
def extract_game_max(pulls) do
pulls
|> String.split("; ", trim: true)
|> Enum.reduce(@default_pull_max, &extract_pull_max/2)
end
def extract_pull_max(pull, acc) do
pull
|> String.split(", ")
|> Enum.reduce(acc, fn c, acc ->
[int, color] = String.split(c, " ")
color = String.to_atom(color)
int = String.to_integer(int)
Map.put(acc, color, max(int, Map.fetch!(acc, color)))
end)
end
Part 1
With the maxima already stored it was straight forward to calculate the games. I tend to reach for Enum/reduce/3 in these cases more than most (it seems).
def matching_games(games, search) do
Enum.reduce games, 0, fn %{game: game, max: max}, acc ->
if search.red >= max.red && search.blue >= max.blue && search.green >= max.green, do: acc + game, else: acc
end
end
Part 2
This is trivial since the data is already in the right format. Again I find `Enum.reduce` super handy because it already accumulates. def part_2 do
parse_input()
|> Enum.reduce(0, fn %{max: %{red: red, green: green, blue: blue}}, acc ->
acc + (red * green * blue)
end)
end
bjorng
I spent most of the time on the parser, which I implemented using NimbleParsec. I’ve used NimbleParsec before, but it has never became second nature for me, so I still have to refer to the documentation a lot.
This time I learned the hard way that nesting a repeat inside a repeat leads to an infinite loop, such as in this code from my parser:
cubes = repeat(cube)
|> optional(ignore(string("; ")))
|> wrap
subsets = repeat(cubes)
|> wrap
The problem is that the inner repeat will always succeed (by repeating zero times), and therefore the outer repeat will repeat the inner repeat forever.
I solved it by replacing the inner repeat with times with a minimum of one:
cubes = times(cube, min: 1)
|> optional(ignore(string("; ")))
|> wrap
subsets = repeat(cubes)
|> wrap
Solution
https://github.com/bjorng/advent-of-code-2023/blob/main/day02/lib/day02.ex







