christhekeele

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

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

awerment

Just wanted to say I love the use of Integer.parse/1 here!

stevensonmt

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

kip

ex_cldr Core Team

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

bjorng

Erlang Core Team

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

Where Next?

Popular in Challenges Top

sb8244
Note: This topic is to talk about Day 10 of the Advent of Code 2019 . There is a private leaderboard for elixirforum members. You can jo...
New
bjorng
Note: This topic is to talk about Day 16 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can joi...
New
bjorng
Note: This topic is to talk about Day 5 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can join...
New
Aetherus
This topic is about Day 4 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/le...
New
bjorng
Note: This topic is to talk about Day 23 of the Advent of Code. For general discussion about the Advent of Code 2018 and links to topics...
New
bjorng
Note: This topic is to talk about Day 1 of the Advent of Code 2019.
New
woolfred
It is that time of the year again: Advent of Code 2022 :christmas_tree: Day 1 Leaderboard:
New
kwando
Took a while, but another use case for “move vectors” today and pattern matching. :slight_smile: The trick was to first generate a list...
New
New
connorlay
Note by the Moderators: This topic is for general discussion about the Advent of Code 2018. To prevent people from being spoiled about s...
New

Other popular topics Top

peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New

We're in Beta

About us Mission Statement