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

First 10 of 30 Posts Switch mode

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
)

https://git.adamu.jp/adam/AdventOfCode/src/branch/main/2023/day2.exs

christhekeele

christhekeele OP

That Map.merge/3 is a very elegant way to get all maxes in one iteration, I wouldn’t’ve thought of it!

Aetherus

Aetherus

I was thinking maybe this puzzle could be solved using nimble_parsec, but when I tried, I just got lost because I couldn’t understand the documentation and the examples :joy:

Maybe I should just go for yecc.

christhekeele

christhekeele OP

I struggle with the docs for both every time I have to pick them up :sweat:. Would love to see that incarnation if you get around to it!

Aetherus

Aetherus

I have to attend my sister’s wedding ceremony now. Maybe later.

seeplusplus

seeplusplus

I was able to knock this one out much quicker than yesterday’s

red_limit = 12
green_limit = 13
blue_limit = 14

parse_rounds = fn s ->
  s
  |> String.split(";")
  |> Enum.map(
    &Enum.map(
      String.split(&1, ","),
      fn x ->
        [_, count, color] = Regex.run(~r/(\d+) (.+)/, String.trim(x))
        {count, _} = count |> Integer.parse()
        color = color |> String.to_atom()

        {color, count}
      end
    )
  )
end

all_games =
  for line <- IO.stream(),
      line |> String.trim() |> String.length() > 0,
      [_, id, rounds] = Regex.run(~r/Game (\d+): (.+)/, line) do
    {id |> Integer.parse() |> elem(0), parse_rounds.(rounds)}
  end

part1 = fn games ->
  games
  |> Enum.filter(fn {_, games} ->
    Enum.all?(games, fn game ->
      game |> Keyword.get(:green, 0) <= green_limit &&
        game |> Keyword.get(:red, 0) <= red_limit &&
        game |> Keyword.get(:blue, 0) <= blue_limit
    end)
  end)
  |> Enum.map(&elem(&1, 0))
  |> Enum.sum()
end

part2 = fn games ->
  games
  |> Enum.map(
    &(elem(&1, 1)
      |> Enum.reduce(
        {0, 0, 0},
        fn round, {red, green, blue} ->
          {
            max(red, Keyword.get(round, :red, 0)),
            max(green, Keyword.get(round, :green, 0)),
            max(blue, Keyword.get(round, :blue, 0))
          }
        end
      ))
  )
  |> Enum.map(fn {x, y, z} -> x * y * z end)
  |> Enum.sum()
end

Edit: really impressed with the brevity from some of the solutions I’ve seen for the last two days. Great stuff all.

midouest

midouest

Did a little more work than I needed to in part 1, but it made the modifications for part 2 pretty easy! This was a nice day 2 problem :slight_smile:

Part 1
defmodule Part1 do
  def answer(text, %{} = bag) do
    text
    |> String.trim_trailing()
    |> String.splitter("\n")
    |> Enum.map(fn line ->
      Regex.scan(~r/(\d+) (\w+)/, line)
      |> Enum.map(fn [_, n, color] ->
        {String.to_integer(n), color}
      end)
      |> Enum.reduce(%{}, fn {n, color}, acc ->
        Map.put(acc, color, max(n, Map.get(acc, color, 0)))
      end)
    end)
    |> Enum.with_index(1)
    |> Enum.filter(fn {set, _} ->
      Enum.all?(set, fn {key, value} -> value <= bag[key] end)
    end)
    |> Enum.map(&elem(&1, 1))
    |> Enum.sum()
  end
end

bag = %{"red" => 12, "green" => 13, "blue" => 14}
Part1.answer(input, bag)strong text
Part 2
defmodule Part2 do
  def answer(text) do
    text
    |> String.trim_trailing()
    |> String.splitter("\n")
    |> Enum.map(fn line ->
      Regex.scan(~r/(\d+) (\w+)/, line)
      |> Enum.map(fn [_, n, color] ->
        {String.to_integer(n), color}
      end)
      |> Enum.reduce(%{}, fn {n, color}, acc ->
        Map.put(acc, color, max(n, Map.get(acc, color, 0)))
      end)
      |> Map.values()
      |> Enum.product()
    end)
    |> Enum.sum()
  end
end

Part2.answer(input)
mexicat

mexicat

Quite straightforward with Regex.scan and Enum.group_by:

https://github.com/mexicat/aoc-2023/blob/main/lib/aoc/day_02.ex

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
herisson

herisson

Very elegant, I miss a lot of training with pattern matching.
I did not know one could pattern match on the string directly. That must me a good advantage comparing to other programming langages

Where Next?

Trending in Challenges Top

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New

We're in Beta

About us Mission Statement