Advent of Code 2024 - Day 3

Here’s my day 3 code

This was quite easy. I was afraid Part 2 would be “un-regex-able” and was preparing for hand crafting automata but looks like it wasn’t the case. Also, nice to have that Toboggan reference. I love cross-overs.

4 Likes

Part 1:

defmodule Part1 do
  def scan(input) do
    Regex.scan(~r/mul\((\d{1,3}),(\d{1,3})\)/, input, capture: :all_but_first)
    |> Enum.reduce(0, fn [a, b], acc ->
      acc + String.to_integer(a) * String.to_integer(b)
    end)
  end
end

Part 2:

defmodule Part2 do
  def scan(input, enabled \\ :enabled, acc \\ 0)

  def scan(input, :enabled, acc) do
    case Regex.split(~r/don't\(\)/, input, parts: 2) do
      [valid, rest] -> scan(rest, :disabled, acc + Part1.scan(valid))
      [valid] -> acc + Part1.scan(valid)
    end
  end

  def scan(input, :disabled, acc) do
    case Regex.split(~r/do\(\)/, input, parts: 2) do
      [_invalid, rest] -> scan(rest, :enabled, acc)
      [_invalid] -> acc
    end
  end
end
1 Like

Here are my solutions for today. (if another thread for this pops up, I’ll try to delete it. My version of this thread is still pending approval :laughing: )

Part 1:

defmodule Day3.Part1 do
  def solve() do
    File.stream!("03/input.txt")
    |> Enum.reduce(
      0,
      fn line, acc ->
        acc +
          (~r/mul\((\d{1,3}),(\d{1,3})\)/
           |> Regex.scan(line |> String.trim(), capture: :all_but_first)
           |> Enum.reduce(
             0,
             fn [a, b], acc -> acc + String.to_integer(a) * String.to_integer(b) end
           ))
      end
    )
    |> IO.puts()
  end
end

Day3.Part1.solve()

Part 2:

defmodule Day3.Part2 do
  defp sum_part(part) do
    ~r/mul\((\d{1,3}),(\d{1,3})\)/
    |> Regex.scan(part, capture: :all_but_first)
    |> Enum.reduce(
      0,
      fn [a, b], acc -> acc + String.to_integer(a) * String.to_integer(b) end
    )
  end

  def solve() do
    File.stream!("03/input.txt")
    |> Enum.reduce(
      {0, true},
      fn line, {acc, start_enabled?} ->
        {acc +
           (line
            |> String.trim()
            |> String.split("do")
            |> (fn parts ->
                  if start_enabled? do
                    parts
                  else
                    parts
                    |> Enum.drop(1)
                  end
                end).()
            |> Enum.filter(&(not String.starts_with?(&1, "n't()")))
            |> Enum.map(&sum_part/1)
            |> Enum.sum()),
         ~r/do\(\)/
         |> Regex.scan(line, capture: :all, return: :index)
         |> List.last()
         |> List.last()
         |> elem(0) >
           ~r/don't\(\)/
           |> Regex.scan(line, capture: :all, return: :index)
           |> List.last()
           |> List.last()
           |> elem(0)}
      end
    )
    |> elem(0)
    |> IO.puts()
  end
end

Day3.Part2.solve()
2 Likes

I haven’t used regex in a while - made a few dumb mistakes before getting it right, lol.

1 Like

Thanks for your regex. It’s the first time I saw (?>pattern) in a regex.

Here’s my solution:

Part 1

~r/
  (?<=mul\()  # prefixed by "mul("  (positive lookbehind, not captured)
  (\d{1,3})   # max-3-digit number  (captured)
  ,           # matches a comma     (not captured)
  (\d{1,3})   # max-3-digit number  (captured)
  (?=\))      # suffixed by ")"     (positive lookahead, not captured)
/x
|> Regex.scan(puzzle_input, capture: :all_but_first)
|> List.flatten()
|> Enum.map(&String.to_integer/1)
|> Enum.chunk_every(2)
|> Enum.map(fn [a, b] -> a * b end)
|> Enum.sum()

Part 2

~r/
  (do\(\))     # matches "do()" (captured)
  |            # or
  (don't\(\))  # matches "don't()" (captured)
  |            # or
  (?<=mul\()(\d{1,3}),(\d{1,3})(?=\))  # matches the same pattern as in Part 1, captures only the numbers
/x
|> Regex.scan(puzzle_input, capture: :all_but_first)
|> Enum.map(fn
  ["do()"] -> :on
  ["", "don't()"] -> :off
  ["", "", a, b] -> {String.to_integer(a), String.to_integer(b)}
end)
|> Enum.reduce({0, :on}, fn
  :on, {sum, _} -> {sum, :on}
  :off, {sum, _} -> {sum, :off}
  {a, b}, {sum, :on} -> {sum + a * b, :on}
  _, acc -> acc
end)
|> elem(0)
3 Likes

Hey!

This is my solution for Day 03.

Part 1

Regex.scan(~r/mul\((\d+),(\d+)\)/, puzzle_input, capture: :all_but_first)
|> Enum.map(fn [a, b] ->
  String.to_integer(a) * String.to_integer(b)
end)
|> Enum.sum()

Part 2

Regex.scan(~r/mul\((-?\d+),(-?\d+)\)|do(?:n't)?\(\)/, puzzle_input)
|> Enum.reduce({0, :enabled}, fn
  ["don't()"], {count, _status} ->
    {count, :disabled}

  ["do()"], {count, _status} ->
    {count, :enabled}

  [_text, a, b], {count, :enabled} ->
    result = String.to_integer(a) * String.to_integer(b)

    {result + count, :enabled}

  [_text, _a, _b], {count, :disabled} ->
    {count, :disabled}
end)
|> elem(0)
5 Likes

I just realized I could just have one function with regex as an additional parameter. (In case of first, adding true on both cases unlocks the whole list for processing)

I love it. Saves me a lot of “emptiness” lol.

1 Like

I went for a solution using regexes and regretted it almost immediately. It took me a while to realize that Regex.run/3 with the :global option will not return multiple solutions (as :re.run/3 would), but that I needed to use Regex.scan/3. Fortunately, having invested a lot of time solving part 1 with a regex, it turned out it was possible to solve also part 2 with a regex.

Having tried regexes, I decided to implement a solution in Erlang using the binary syntax to do the parsing:

UPDATE: After looking at the other solutions, I realized that I only looked for do and don't instead of do() and don't(). That happened to produce the correct result (at least for my input), but I’ve now updated my programs to match the parens too.

4 Likes

This is the way.

3 Likes

Just some average-ugly regexes.

3 Likes

More regexes.

I’m not sure why do produces ["", "", "do()"] and don't produces ["", "", "", "don't()"]. The number of empty strings is consistent, but my regex-fu is not enough to figure out why / prevent it.

def part1(input) do
  Regex.scan(~r/mul\((\d+),(\d+)\)/, input, capture: :all_but_first)
  |> Enum.map(fn [a, b] -> String.to_integer(a) * String.to_integer(b) end)
  |> Enum.sum()
end

def part2(input) do
  Regex.scan(~r/mul\((\d+),(\d+)\)|(do\(\))|(don't\(\))/, input, capture: :all_but_first)
  |> Enum.reduce({0, :process}, fn
    ["", "", "do()"], {sum, _} -> {sum, :process}
    ["", "", "", "don't()"], {sum, _} -> {sum, :skip}
    _, {sum, :skip} -> {sum, :skip}
    [a, b], {sum, :process} -> {sum + String.to_integer(a) * String.to_integer(b), :process}
  end)
  |> elem(0)
end

Empty placeholder for the preceding “do()” in the regex |-s. If you swap them then you will see the reverse scenario

1 Like

Because (do\(\)) is the 3rd capture group, and (don't\(\)) is the 4th in your regex.

2 Likes

I wanted to do binary parsing as well but the regexes are too convenient here, so quick solution like a lot in this thread:

defmodule AdventOfCode.Solutions.Y24.Day03 do
  alias AoC.Input

  def parse(input, _part) do
    String.trim(Input.read!(input))
  end

  def part_one(problem) do
    ~r/mul\(([0-9]{1,3}),([0-9]{1,3})\)/
    |> Regex.scan(problem)
    |> Enum.reduce(0, fn [_, a, b], acc ->
      String.to_integer(a) * String.to_integer(b) + acc
    end)
  end

  def part_two(problem) do
    ~r/(mul\(([0-9]{1,3}),([0-9]{1,3})\)|do\(\)|don't\(\))/
    |> Regex.scan(problem)
    |> Enum.reduce({0, true}, fn
      ["do()" | _], {acc, _enabled?} -> {acc, true}
      ["don't()" | _], {acc, _enabled?} -> {acc, false}
      [_, _, a, b], {acc, true} -> {String.to_integer(a) * String.to_integer(b) + acc, true}
      [_, _, _, _], {acc, false} -> {acc, false}
    end)
    |> elem(0)
  end
end

2 Likes

@code-shoily @Aetherus makes sense, thanks! But how to prevent it while still using capture: :all_but_first? :smirk:

Maybe it’s not something that’s worth worrying about :man_shrugging:

My todays solution was a much quicker than the other days, and I am posting this in a hurry, trying to get into the next meeting in time…

Though I have to speak a work of warning:

:warning: This solution is RegEx free :warning:

3 Likes

Part 2 threw me for a loop because the inputs were all separated by lines. But overall pretty fun problem

defmodule AOC.Y2024.Day3 do
  @moduledoc false

  use AOC.Solution

  @mul_regex ~r/mul\((\d+),(\d+)\)/
  @do_regex ~r/do\(\)/
  @dont_regex ~r/don\'t\(\)/

  @impl true
  def load_data() do
    Data.load_day(2024, 3)
    |> Enum.join()
  end

  @impl true
  def part_one(data) do
    Regex.scan(@mul_regex, data, capture: :all_but_first)
    |> Enum.map(fn v -> Enum.map(v, &String.to_integer(&1)) end)
    |> General.map_sum(fn [a, b] -> a * b end)
  end

  @impl true
  def part_two(data) do
    data
    |> get_do_instructions()
    |> Enum.concat(get_dont_instructions(data))
    |> Enum.concat(get_mul_instructions(data))
    |> Enum.sort(fn a, b -> elem(a, 0) < elem(b, 0) end)
    |> Enum.reduce({0, :do}, fn
      {_, :do}, {acc, _} -> {acc, :do}
      {_, :dont}, {acc, _} -> {acc, :dont}
      {_, :mul, a, b}, {acc, :do} -> {acc + a * b, :do}
      {_, :mul, _, _}, {acc, :dont} -> {acc, :dont}
    end)
    |> elem(0)
  end

  defp get_mul_instructions(data),
    do:
      Regex.scan(@mul_regex, data, return: :index)
      |> Enum.map(fn [{mul_idx, _}, {f, fl}, {s, sl}] ->
        {mul_idx, :mul, data |> String.slice(f, fl) |> String.to_integer(),
         data |> String.slice(s, sl) |> String.to_integer()}
      end)

  defp get_do_instructions(data),
    do:
      Regex.scan(@do_regex, data, capture: :first, return: :index)
      |> Enum.flat_map(& &1)
      |> Enum.map(fn {i, _} -> {i, :do} end)

  defp get_dont_instructions(data),
    do:
      Regex.scan(@dont_regex, data, capture: :first, return: :index)
      |> Enum.flat_map(& &1)
      |> Enum.map(fn {i, _} -> {i, :dont} end)
end
1 Like

Nice!

This made me realize that there is no case in my input where there would be a 4 digit number in the mul expressions.

No Regex and no sub-binaries
  defp parse_instructions("mul(" <> rest, acc) do
    parse_int_1(rest, 0, acc)
  end

  defp parse_instructions("do()" <> rest, acc) do
    parse_instructions(rest, [:do | acc])
  end

  defp parse_instructions("don't()" <> rest, acc) do
    parse_instructions(rest, [:dont | acc])
  end

  defp parse_instructions(<<_, rest::bytes>>, acc) do
    parse_instructions(rest, acc)
  end

  defp parse_instructions(<<>>, acc) do
    :lists.reverse(acc)
  end

  defp parse_int_1(<<c, rest::bytes>>, inner_acc, outer_acc) when c in ?0..?9 do
    parse_int_1(rest, inner_acc * 10 + c - ?0, outer_acc)
  end

  defp parse_int_1(<<?,, rest::bytes>>, inner_acc, outer_acc) do
    parse_int_2(rest, 0, [inner_acc | outer_acc])
  end

  defp parse_int_1(<<_, rest::bytes>>, _inner_acc, outer_acc) do
    parse_instructions(rest, outer_acc)
  end

  defp parse_int_2(<<c, rest::bytes>>, inner_acc, outer_acc) when c in ?0..?9 do
    parse_int_2(rest, inner_acc * 10 + c - ?0, outer_acc)
  end

  defp parse_int_2(<<?), rest::bytes>>, r, [l | outer_acc]) do
    parse_instructions(rest, [{:mul, l, r} | outer_acc])
  end

  defp parse_int_2(<<_, rest::bytes>>, _inner_acc, [_l | outer_acc]) do
    parse_instructions(rest, outer_acc)
  end

Full: aoc2024/lib/day3.ex at master · ruslandoga/aoc2024 · GitHub

2 Likes