Advent of Code 2024 - Day 3

My version would incorrectly accept 4 digit operands in the mul directives.

Specification clearly said “X and Y are each 1-3 digit numbers”.

I just relied on the input being not malformed in that regard :smiley:

I used NimbleParsec to build a parser:

defmodule Parser do
  import NimbleParsec

  disable =
    ignore(string("don't()"))
    |> tag(:disable)

  enable =
    ignore(string("do()"))
    |> tag(:enable)

  operand = integer(max: 3, min: 1)

  mul =
    ignore(string("mul("))
    |> concat(operand)
    |> ignore(string(","))
    |> concat(operand)
    |> ignore(string(")"))
    |> tag(:mul)

  instruction = choice([disable, enable, mul])

  instructions =
    eventually(instruction)
    |> repeat()

  defparsec(:parse, instructions |> eventually(eos()))
end
5 Likes

Today I learned Regex.split with include_captures: true option :mage:

Part1

defmodule Advent.Y2024.Day03.Part1 do
  def run(puzzle) do
    Regex.scan(~r|mul\(\d+,\d+\)|, puzzle)
    |> Enum.map(fn [s] -> evaluate_mul(s) end)
    |> Enum.sum()
  end

  def evaluate_mul(s) do
    [_, n1, n2, _] = String.split(s, ["(", ",", ")"])
    String.to_integer(n1) * String.to_integer(n2)
  end
end

Part2

defmodule Advent.Y2024.Day03.Part2 do
  alias Advent.Y2024.Day03.Part1

  def run(puzzle) do
    Regex.split(~r/mul\(\d+,\d+\)|(do\(\))|(don't\(\))/, puzzle, include_captures: true)
    |> Enum.reduce(%{sum: 0, enabled: true}, fn
      "do()", acc ->
        %{acc | enabled: true}

      "don't()", acc ->
        %{acc | enabled: false}

      s, acc = %{enabled: true, sum: sum} ->
        if String.match?(s, ~r|mul\(\d+,\d+\)|) do
          %{acc | sum: sum + Part1.evaluate_mul(s)}
        else
          acc
        end

      _, acc ->
        acc
    end)
    |> Map.get(:sum)
  end
end
2 Likes

I made codepoints and used extensive pattern matching instead of regex. :sweat_smile:

  defp find_muls([], solution), do: solution

  defp find_muls(["m", "u", "l", "(", a1, ",", b1, ")" | rest], solution) do
    case {Integer.parse(a1), Integer.parse(b1)} do
      {{n1, _}, {n2, _}} -> find_muls(rest, solution + n1 * n2)
      _ -> find_muls(rest, solution)
    end
  end

  defp find_muls(["m", "u", "l", "(", a1, ",", b1, b2, ")" | rest], solution) do
    case {Integer.parse(a1), Integer.parse(b1 <> b2)} do
      {{n1, _}, {n2, _}} -> find_muls(rest, solution + n1 * n2)
      _ -> find_muls(rest, solution)
    end
  end

...

Posting just a part of it, it’s too big. :japanese_goblin:
For part 2 I added a factor argument that goes to 1 when do is pattern matched, goes to 0 when don’t. Needless to say, I’m not satisfied with my solution. :bug:

I once wrote a lexer using Regex.split with include_captures: true and parts: 2, trim: false.

1 Like

Part1 i wont care about sharing. But Part2., i tried to scan over this thread and couldnt find something similar, but the solution i found was a fold left and pattern matching on functions and a tuple accumulator

actually day3 part2 was pretty cool task <3

really liked your part2 impl :slight_smile: <3

1 Like

Took some time to fix my initial code; but really wanted to make it within the cutoff limit of the code box at this forum ;). 24 lines, with official formatting that is.

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

  def parse(input, _part), do: Input.read!(input)

  def part_one(problem), do: problem |> sanitize(~w(mul)) |> exec_calc() |> elem(1)

  def part_two(problem), do: problem |> sanitize(~w(mul do don't)) |> exec_calc() |> elem(1)

  defp sanitize(input, keywords),
    do:
      ("(" <> Enum.join(keywords, "|") <> ")\\((?:([0-9]{1,3}),([0-9]{1,3}))?\\)")
      |> Regex.compile!()
      |> Regex.scan(input, capture: :all_but_first)

  defp exec_calc(input),
    do:
      Enum.reduce(input, {:active, 0}, fn
        ["mul", x, y], {:active, sum} -> {:active, sum + String.to_integer(x) * String.to_integer(y)}
        ["do"], {_, sum} -> {:active, sum}
        ["don't"], {_, sum} -> {:inactive, sum}
        _, acc -> acc
      end)
end
2 Likes

Ok. At least part1 was way more easy for me than yesterday. Still working on part2

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

  def parse(input, _part) do
    garbage = Input.read!(input)
    Regex.scan(~r/mul\((\d+),(\d+)\)/, garbage)
  end

  def part_one(problem) do
    problem
    |> List.foldl(0, fn([_, x, y], acc) ->
     acc + String.to_integer(x) * String.to_integer(y)
    end)
  end
3 Likes

i love how compact it is <3

1 Like

Can definitely be tidied up and simplified, but still exploring different ways to solve a problem in Elixir

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

  def part_2(input) do
    ~r/mul\((\d{1,3}),(\d{1,3})\)|don't\(\)|do\(\)/
      |> Regex.scan(input)
      |> Enum.reduce({:enabled, 0}, fn row, acc ->
        case process(row) do
          :enabled ->
            {:enabled, elem(acc, 1)}
          :disabled -> 
            {:disabled, elem(acc, 1)}
          x ->
            case elem(acc, 0) do
              :disabled ->
                {:disabled, elem(acc, 1)}
              :enabled ->
                {:enabled, x + elem(acc, 1)}
            end
        end
      end)    
  end

  def process(row) when is_list(row) and length(row) == 1 do
    case row do
      ["don't()"] ->
        :disabled
      ["do()"] ->
        :enabled
    end
  end

  def process(row) when is_list(row) and length(row) == 3 do
    [_ | rest] = row
    rest
    |> Enum.map(&String.to_integer/1)
    |> Enum.product()
  end  
end
#!/usr/bin/env elixir

defmodule Day3 do
  def run(test_file) do
    list = File.read!(test_file)
    IO.puts("Total Product Sum: #{total_product_sum(list)}")
    IO.puts("Total Processed Sum: #{total_processed_sum(list)}")
  end

  def total_product_sum(list) do
    regex = ~r/mul\((\d+),(\d+)\)/

    list
    |> extract_phrases(regex)
    |> Stream.map(&extract_numbers_product/1)
    |> Stream.map(fn {a, b} -> a * b end)
    |> Enum.sum()
  end

  def total_processed_sum(list) do
    regex = ~r/(do\(\)|don't\(\)|mul\((\d+),(\d+)\))/

    list
    |> extract_phrases(regex)
    |> process_phrases()
    |> Stream.map(fn {a, b} -> a * b end)
    |> Enum.sum()
  end

  defp extract_phrases(content, regex), do: Regex.scan(regex, content)

  defp extract_numbers_product([_, a, b]), do: {String.to_integer(a), String.to_integer(b)}

  defp process_phrases(phrases) do
    Enum.reduce(phrases, {true, []}, fn
      # Enable mul instructions
      ["do()", _], {_, acc} ->
        {true, acc}

      # Disable mul instructions
      ["don't()", _], {_, acc} ->
        {false, acc}

      # Handle mul(a,b) when mul is enabled
      ["mul(" <> _, _, a, b], {true, acc} when a != nil and b != nil ->
        {true, [{String.to_integer(a), String.to_integer(b)} | acc]}

      # Handle mul(a,b) when mul is disabled (ignore)
      ["mul(" <> _, _, _, _], {false, acc} ->
        {false, acc}

      _, acc ->
        acc
    end)
    |> elem(1)
  end
end

Day3.run("./testdata.txt")

I have been naughty and just ran eval string on every mul(x,y) I collected

3 Likes

First time posting here I see. What an entry!

2 Likes

Approach: regex + code eval:

defmodule Aoc2024.Day03 do
  def part1(file) do
    Regex.scan(~r/mul\(\d+,\d+\)/, File.read!(file))
    |> Enum.reduce(0, fn [code], sum -> sum + eval(code) end)
  end

  def part2(file) do
    Regex.scan(~r/don't\(\)|do\(\)|mul\(\d+,\d+\)/, File.read!(file))
    |> Enum.reduce({0, true}, fn
      ["do" <> rest], {sum, _} -> {sum, rest == "()"}
      [code], {sum, add?} -> {sum + ((add? && eval(code)) || 0), add?}
    end)
    |> then(&elem(&1, 0))
  end

  def eval(code), do: elem(Code.eval_string("Aoc2024.Day03." <> code, [], __ENV__), 0)
  def mul(a, b), do: a * b
end
1 Like

:man_facepalming:

1 Like

Yeah, I always refactor those to (f.ex. for a tri-tuple):

|> then(fn {x, _y, _z} -> x end)

It’s just clearer and easier to read.

3 Likes

Ha yeah I actually did the opposite here. I was actually refactoring |> then(fn {sum, _} -> sum end) (which I also prefer for readability) to be shorter. Only after I posted did I see what I’d done.

Wanted to try out something new and used nimble_parsec for the first time.

5 Likes