Advent of Code 2022 - Day 5

File.read!("input.txt") :slight_smile:

2 Likes

The last time I needed to deal with a matrix I was able to use Ruby’s stdlib and I definitely missed it here.

I use the formatter with elixir-ls in vim, and although it removes trailing spaces from the end of lines in general, it does not do it from within strings. So it must be VS Code, @IloSophiep.

Also, parsing the input is half of the fun. Today was a great opportunity to flex your <<binary>> muscles.

I wasn’t at a computer earlier but have now checked this.

@IloSophiep It turns out that it isn’t the Elixir formatter, but it is the ElixirLS VS Code extension doing this. There is the VS Code setting Files: Trim Trailing Whitespace which apparently defaults to false, since I have never changed this setting.

However, I see that the ElixirLS extension overrides this to true. Since ElixirLS overrides this, VS Code removes the trailing whitespace without regard to semantic meaning.

2 Likes

Okay, you got me. I circled back and parsed the stacks, but I grumbled. Lol. Mainly, I wanted to transpose the input, so that’s what I ended up doing.

  @doc """
  List of the initial stacks and their crates
  """
  @spec initial_stacks() :: [stack()]
  def initial_stacks() do
    Utilities.read_data(5, trim: false)
    # Get the stack data only, leaving off the instructions
    |> Enum.take_while(fn s -> s != "\n" end)
    # Chunk every line into "column" elements
    |> Enum.map(fn line ->
      line
      |> String.codepoints()
      |> Enum.chunk_every(4)
      |> Enum.map(&Enum.join/1)
      |> Enum.map(&String.trim/1)
    end)
    # Transpose the lines so as to turn each line into a single stack,
    # with the first element of the list being the top stack element
    |> Utilities.transpose()
    |> Enum.map(fn crates ->
      crates
      # Drop the stack number from the end
      |> Utilities.drop_last()
      # Get rid of the "empty" crates on top of the real crates
      |> Enum.filter(&Utilities.non_empty?/1)
      # Parse the crates to just get the name
      |> Enum.map(fn "[" <> <<crate :: binary-1>> <> "]" -> crate end)
    end)
    |> Enum.map(&Stack.new/1)
  end

I could probably compress this a little (fusing some maps and filters) but will leave it as is for now. Curious if anyone else did it similarly, though I suppose it’s implicitly done this way no matter what.

This gives:

Thank you very much! I added this to my settings.json

{
    "[elixir]": {
        "files.trimTrailingWhitespace": false,
    }
}

and now i can properly write the multiline docs without my whitespaces being removed! Thank you (and @stevensonmt and @kwando) for the help! :heart:

late to the party… I hate my solution already after seeing some of the more elegant ways here :grimacing:

defmodule ExAOC2022.Day5 do
  @input "./lib/day5_input.txt"

  def puzzle1() do
    {initial_state, instructions} = init()

    new_state = Enum.reduce(instructions, initial_state, &run_instructions/2)

    get_top_boxes(new_state)
  end

  def puzzle2() do
    {initial_state, instructions} = init()

    new_state = Enum.reduce(instructions, initial_state, &(run_instructions(&1, &2, true)))

    get_top_boxes(new_state)
  end

  #
  #
  # private shenanigans
  #
  # loads data and sets up initial state
  defp init() do
    [port, _, instructions, _rest] =
      @input
      |> file_by_line()
      |> Enum.chunk_by(&(&1 == ""))

    {parse_state(Enum.reverse(port)), instructions}
  end

  # runs instructions duh
  defp run_instructions(instr, state, batch_process? \\ false) do
    [[times], [from_id], [to_id]] = Regex.scan(~r/\d+/, instr)

    src_stack = state[from_id]
    trgt_stack = state[to_id]

    {updated_trgt, updated_src} = for _i <- 1..String.to_integer(times), reduce: {[], src_stack} do
      {trgt_acc, src_acc} ->
        {box, new_src} = pop(src_acc)
        new_trgt = push(trgt_acc, box)

        {new_trgt, new_src}
    end

    updated_trgt = if batch_process?, do: Enum.reverse(updated_trgt), else: updated_trgt

    state
    |> Map.put(from_id, updated_src)
  #
  #
  # private shenanigans
  #
  # loads data and sets up initial state
  defp init() do
    [port, _, instructions, _rest] =
      @input
      |> file_by_line()
      |> Enum.chunk_by(&(&1 == ""))

    {parse_state(Enum.reverse(port)), instructions}
  end

  # runs instructions duh
  defp run_instructions(instr, state, batch_process? \\ false) do
    [[times], [from_id], [to_id]] = Regex.scan(~r/\d+/, instr)

    src_stack = state[from_id]
    trgt_stack = state[to_id]

    {updated_trgt, updated_src} = for _i <- 1..String.to_integer(times), reduce: {[], src_stack} do
      {trgt_acc, src_acc} ->
        {box, new_src} = pop(src_acc)
        new_trgt = push(trgt_acc, box)

        {new_trgt, new_src}
    end

    updated_trgt = if batch_process?, do: Enum.reverse(updated_trgt), else: updated_trgt

    state
    |> Map.put(from_id, updated_src)
    |> Map.put(to_id, updated_trgt ++ trgt_stack)
  end

  # returns top box from each stack
  defp get_top_boxes(state) do
    for {_, stack} <- state do
      {top_item, _} = pop(stack)
      top_item
    end
    |> Enum.join
  end

  defp parse_state([head | rest]) do
    initial_map = create_map(head)

    rest
    |> Enum.reduce(initial_map, fn row, state ->
      parsed_row = parse_row(row)

      for {val, stack_id} <- parsed_row, reduce: state do
        acc ->
          if val !== "", do: Map.update(acc, "#{stack_id}", [], fn stack -> push(stack, val) end), else: acc
      end
    end)
  end

  # turns a state row into something usable
  defp parse_row(row) do
    row
    |> String.graphemes
    |> Enum.chunk_every(4)
    |> Enum.map(&extract_box_id/1)
    |> Enum.with_index(1)
  end

  defp extract_box_id(box_info), do: box_info |> Enum.join() |> String.replace(~r/[^A-Z]/, "")

  # initializes the state map based on the input data
  defp create_map(head) do
    head
    |> String.trim()
    |> String.split()
    |> Map.from_keys([])
  end

  # stack stack stack
  defp push(stack, item) do
    [item | stack]
  end

  defp pop([item | rest]) do
    {item, rest}
  end

  defp file_by_line(file) do
    file
    |> File.read!()
    |> String.split(~r/\R/)
  end
end

So good!

Parsing

[stack, moves] =
  input
  |> String.split(~r/\n\n/)

[_ | stack] =
  stack
  |> String.split("\n", trim: true)
  |> Enum.reverse()

stack =
  stack
  |> Enum.map(&String.codepoints/1)
  |> Enum.zip_with(& &1)
  |> Enum.reject(fn
    ["[" | _] -> true
    ["]" | _] -> true
    [" " | _] -> true
    _ -> false
  end)
  |> Enum.map(fn row ->
    row |> Enum.reverse() |> Enum.reject(fn crate -> crate == " " end)
  end)
  |> Enum.with_index(fn stack, i -> {i + 1, stack} end)
  |> Enum.into(%{})

moves =
  moves
  |> String.split("\n", trim: true)
  |> Enum.map(fn command ->
    %{"amount" => amount, "from" => from, "to" => to} =
      Regex.named_captures(~r/move (?<amount>\d+) from (?<from>\d+) to (?<to>\d+)/, command)

    %{
      amount: String.to_integer(amount),
      from: String.to_integer(from),
      to: String.to_integer(to)
    }
  end)

Part 1

moves
|> Enum.reduce(stack, fn move, acc ->
  {crates, rest} = Map.get(acc, move.from) |> Enum.split(move.amount)
  acc
  |> Map.put(move.from, rest)
  |> Map.update(move.to, [], & Enum.reverse(crates) ++ &1)
end)
|> Enum.to_list()
|> Enum.sort_by(& elem(&1, 0))
|> Enum.map(fn {_, [first | _]} -> first end)
|> Enum.join()

Part 2

moves
|> Enum.reduce(stack, fn move, acc ->
  {crates, rest} = Map.get(acc, move.from) |> Enum.split(move.amount)
  acc
  |> Map.put(move.from, rest)
  |> Map.update(move.to, [], & crates ++ &1)
end)
|> Enum.to_list()
|> Enum.sort_by(& elem(&1, 0))
|> Enum.map(fn {_, [first | _]} -> first end)
|> Enum.join()

Phew… this one was hard for me. I’m not really used to functional programming, so what I would normally do in a stateful object-oriented programming language didn’t work here. Anyway, here’s my solution:

Input parsing was a struggle, but I found that that Enum.reduce was a very nice way of handling it once it got in. I had written out a first solution that took each part separately, but I realized there was some code duplication that only differed by the reducing function, so I refactored it out as shared code with a function parameter, that I quite like. Still feel that I could simplify some of the pipe commands to be cleaner/better aligned.

defmodule Advent.Day5 do

  def debugger(thing) do
    IO.puts(thing)
    thing
  end

  # Read the file

  def getInput() do
    File.read!("inputs/day-5-input.txt")
    |> String.split("\n\n")
    |> parseInput
  end

  def parseInput([state, moves]), do: {parseState(state), parseMoves(moves)}

  def parseState(bundle) do
    bundle
    |> String.replace(~r"[\[\]]", " ")
    |> String.split("\n")
    |> Enum.map(fn line ->
      line
      |> String.codepoints
      |> Enum.chunk_every(4)
      |> Enum.map(fn [_, char | _rest] ->
        String.trim(char)
      end)
    end)
    |> List.zip
    |> Enum.map(fn col ->
      Tuple.to_list(col) |> Enum.filter(&("" != &1)) |> List.pop_at(-1)
    end)
    |> Map.new
  end

  def parseMoves(bundle) do
    bundle
    |> String.split("\n")
    |> Enum.map(fn line ->
      ["move", count, "from", from, "to", to] = String.split(line, " ")
      { String.to_integer(count), from, to }
    end)
  end

  # Part 1
  def single_move(state, 0, _from, _to), do: state
  def single_move(state, count, from, to) do
    [crate | rest] = state[from]
    bottom = state[to]
    state
    |> Map.put(from, rest)
    |> Map.put(to, [crate | bottom])
    |> single_move(count - 1, from, to)
  end

  # Part 2
  def multi_move(state, count, from, to) do
    {crates, rest} = Enum.split(state[from], count)
    bottom = state[to]
    state
    |> Map.put(from, rest)
    |> Map.put(to, crates ++ bottom)
  end

  # Shared

  def sequnce_moves(reducer) do
    {state, moves} = getInput()
    Enum.reduce(moves, state, fn {count, from, to}, state ->
      reducer.(state, count, from, to)
    end)
    |> getTop
    |> Enum.join("")
  end

  def getTop(state) do
    for i <- ?1..?9, do: state |> Map.get(<<i>>) |> List.first
  end

end

Advent.Day5.sequnce_moves(&Advent.Day5.single_move/4) |> IO.puts

Advent.Day5.sequnce_moves(&Advent.Day5.multi_move/4) |> IO.puts

Getting harder. I’m using Advent of Code to get more familiar with Elixir and functional language and until now it was very pleasant thanks to Elixir amazing stdlib.

But I’m hitting the conceptual wall today. An operation thas is trivial in many languages (pop a few item from a container, put them in another) became incredibly difficult with Elixir and its immutable structure.

I can totally relate to what you wrote. It was exactly the same for me. I’m fairly happy with my solution I posted above, but it took me a while to get there.