Advent of Code 2022 - Day 5

Today’s challenge for me was about using reduce:

defmodule Prob5 do
  def move([[h1 | rest] = _list1, list2]) do
    [rest, [h1 | list2]]
  end

  def move_n([l1, l2], n) do
    Enum.reduce(1..n, [l1, l2], fn _, acc -> move(acc) end)
  end

  def single_instruction(input, [n, from_ind, to_ind], move_function) do
    from_ind = from_ind - 1
    to_ind = to_ind - 1
    [from, to] = move_function.([Enum.at(input, from_ind), Enum.at(input, to_ind)], n)
    input
    |> List.replace_at(from_ind, from)
    |> List.replace_at(to_ind, to)
  end

  def parse_line(line) do
    [[n], [from_ind], [to_ind]] = Regex.scan(~r/\d+/, line)
    Enum.map([n, from_ind, to_ind], &String.to_integer/1)
  end

  def move_n_part2([l1, l2], n) do
    to_move = Enum.take(l1, n)
    [Enum.drop(l1, n), to_move ++ l2]
  end
end

I manually parsed the first part of the input into lists, like thus:
test_input = [[:n, :z], [:d, :c, :m], [:p]]

And then parsed the 2nd part of the input into simple lists, and finally ran it through reduce:

Enum.reduce(
  instructions, 
  input, 
  fn instruction, input -> Prob5.single_instruction(input, instruction, &Prob5.move_n/2) end)

… using the 2 move functions - one for each part.

Let me know how this can be improved!

Thanks

I spent more time than I would have only if I hadn’t updated the “initial state” instead of “accumulator” inside that reducer. It was silly of me and thanks to dbg, copying the example data, and 10 minutes of exploration, I got back on track. Cute of me to think I could get away with a _ in lieu of acc lol.

Here’s the outcome: advent_of_code/day_05.ex at master · code-shoily/advent_of_code · GitHub

1 Like

Biggest challenge for me was just parsing the input, which is pretty typical for me.
I probably went overboard spinning up a GenServer but it’s what I always think of when there’s some “moves” or “ops” that have to be tracked.

defmodule Day5 do
  defmodule Input do
    def sample_data() do
      """
          [D]    
      [N] [C]    
      [Z] [M] [P]
       1   2   3 

      move 1 from 2 to 1
      move 3 from 1 to 3
      move 2 from 2 to 1
      move 1 from 1 to 2
      """
    end

    def load() do
      ReqAOC.fetch!({2022, 05, System.fetch_env!("AOC2022Session")})
    end

    def parse(input) do
      [stacks, moves] = input |> String.split("\n\n", trim: true) |> IO.inspect()

      stacks =
        stacks
        |> String.split("\n", trim: true)
        |> Enum.drop(-1)
        |> Enum.flat_map(fn line ->
          line |> String.graphemes() |> Enum.drop(1) |> Enum.take_every(4) |> Enum.with_index(1)
        end)
        |> Enum.group_by(&elem(&1, 1))
        |> Enum.map(fn {column, vals} ->
          {column, vals |> Enum.map(&elem(&1, 0)) |> Enum.reject(&Kernel.==(&1, " "))}
        end)
        |> Map.new()

      moves =
        moves
        |> String.split("\n", trim: true)
        |> Enum.map(fn line ->
          line
          |> String.split(["move ", " from ", " to "], trim: true)
          |> Enum.map(&String.to_integer(&1))
        end)

      {stacks, moves} |> IO.inspect()
    end
  end

  defmodule Stacks do
    use GenServer

    @impl true
    def init(stacks) do
      {:ok, stacks}
    end

    def handle_cast({:move_9000, n, from, to}, stacks) do
      new_stacks =
        1..n
        |> Enum.reduce(stacks, fn _, new_stacks ->
          [crate | stack] = Map.get(new_stacks, from)
          dest = Map.get(new_stacks, to)
          Map.put(new_stacks, from, stack) |> Map.put(to, [crate | dest]) |> IO.inspect()
        end)

      {:noreply, new_stacks}
    end

    def handle_cast({:move_9001, n, from, to}, stacks) do
      {moving, staying} = Map.get(stacks, from) |> Enum.split(n)

      new_stacks =
        Map.put(stacks, from, staying)
        |> Map.update!(to, fn dest -> moving ++ dest end)

      {:noreply, new_stacks}
    end

    def handle_call(:crates_on_top, _from, stacks) do
      tops =
        stacks
        |> Enum.map(fn
          {_col, [hd | rest]} -> hd
          {_col, []} -> :empty
        end)
        |> Enum.reject(fn crate -> crate == :empty end)

      {:reply, tops, stacks}
    end
  end

  defmodule Part1 do
    def solve({stacks, moves}) do
      {:ok, pid} = GenServer.start_link(Stacks, stacks)

      moves
      |> Enum.each(fn [n, from, to] -> GenServer.cast(pid, {:move_9000, n, from, to}) end)

      GenServer.call(pid, :crates_on_top) |> IO.inspect()
      GenServer.stop(pid)
    end
  end

  defmodule Part2 do
    def solve({stacks, moves}) do
      {:ok, pid} = GenServer.start_link(Stacks, stacks)

      moves
      |> Enum.each(fn [n, from, to] -> GenServer.cast(pid, {:move_9001, n, from, to}) end)

      GenServer.call(pid, :crates_on_top) |> Enum.join("") |> IO.inspect()
      GenServer.stop(pid)
    end
  end
end
1 Like

As always, I am here to advertise Access.

# moving crates (part I)
  Enum.reduce(0..count - 1, acc, fn _, acc ->
    {v, acc} = get_and_update_in(acc, [Access.at(from - 1)], fn [h|t] -> {h, t} end)
    update_in(acc, [Access.at(to - 1)], fn t -> [v|t] end)
  end)
# moving crates (part II)
  {v, acc} = get_and_update_in(acc, [Access.at(from - 1)], fn l -> Enum.split(l, count) end)
  update_in(acc, [Access.at(to - 1)], fn t -> v ++ t end)
7 Likes

Thanks. Learnt something new!

Access is the extremely powerful and most underrated Elixir feature.

5 Likes

Finite number of moves is a synonym of reduce/3 through :slight_smile:

3 Likes

Sure. Wasn’t sure what part 2 was going to be though, so it felt most flexible to do it with a GenServer.

1 Like

I think the use of a module and named functions greatly improves readability. But I wanted to be as close as possible to one-liners.

part1
start = System.monotonic_time(:microsecond)

stacks = File.stream!("input.txt")
  |> Stream.take(8)
  |> Stream.map(fn line ->
    # pos = idx / 4 + 1 and idx = (pos - 1) * 4
    # Get the positions of the stacks going from 1 to 9. "[D]                     [N] [F]    " -> [1, 7, 8]
     Regex.scan(~r/\[\w\]/, line, return: :index) |> Enum.map(fn [{start, _end}] -> div(start,4)+1 end)
    # Get the letters on a line with their respective positions. [1, 7, 8] -> [{8, "F"}, {7, "N"}, {1, "D"}]
     |> Enum.reduce([], fn pos, acc -> crate = String.slice(line, (pos-1)*4+1, 1) ; [{pos, crate} | acc] end)
  end)
  # reverse the list to pile up the crates in the right order starting with the crates at the bottom
  |> Enum.reverse()
  # populate the map representing the stacks starting with an empty map
  |> Enum.reduce(%{}, fn lst, m ->
    Enum.reduce(lst, m, fn {pos, crate}, m -> update_in(m[pos], fn nil -> [crate] ; lst -> [crate | lst] end) end)
  end)
  # |> IO.inspect(label: "init stacks")

File.stream!("input.txt")
|> Stream.drop(10)
|> Stream.map(&String.trim_trailing/1)
|> Stream.map(fn line ->
   Regex.run(~r/^move (\d+) from (\d) to (\d)$/, line, capture: :all_but_first)
   |> Enum.map(&String.to_integer/1)
end)
|> Enum.reduce(stacks, fn [n,src,dst], stacks -> Enum.reduce(1..n, stacks, fn _i, stacks ->
    [crate | crates] = stacks[src]
    stacks
    |> put_in([src], crates) # remove the crate from the source stack
    |> update_in([dst], fn nil -> [crate] ; lst -> [crate | lst] end) # put the crate in the destination stack
  end)
end)
# |> IO.inspect(label: "final stacks")
# transform the map of stacks into a list of stacks in the right order
|> Enum.sort_by(fn {pos, _lst} -> pos end)
# keep only the top crate
|> Enum.map(fn {_pos, [crate | _]} -> crate end)
|> Enum.join("")
|> tap(fn crates -> IO.puts "Crates on top of each stack: #{crates}" end)

elapsed = System.monotonic_time(:microsecond) - start
IO.puts "Job done in #{elapsed} µs"

Part 2 is very similar to part 1. The way of moving multiple crates is simplified since a reduce disappears.

part2
start = System.monotonic_time(:microsecond)

stacks = File.stream!("input.txt")
  |> Stream.take(8)
  |> Stream.map(fn line ->
    # pos = idx / 4 + 1 and idx = (pos - 1) * 4
    # Get the positions of the stacks going from 1 to 9. "[D]                     [N] [F]    " -> [1, 7, 8]
     Regex.scan(~r/\[\w\]/, line, return: :index) |> Enum.map(fn [{start, _end}] -> div(start,4)+1 end)
    # Get the letters on a line with their respective positions. [1, 7, 8] -> [{8, "F"}, {7, "N"}, {1, "D"}]
     |> Enum.reduce([], fn pos, acc -> crate = String.slice(line, (pos-1)*4+1, 1) ; [{pos, crate} | acc] end)
  end)
  # reverse the list to pile up the crates in the right order starting with the crates at the bottom
  |> Enum.reverse()
  # populate the map representing the stacks starting with an empty map
  |> Enum.reduce(%{}, fn lst, m ->
    Enum.reduce(lst, m, fn {pos, crate}, m -> update_in(m[pos], fn nil -> [crate] ; lst -> [crate | lst] end) end)
  end)
  # |> IO.inspect(label: "init stacks")

File.stream!("input.txt")
|> Stream.drop(10)
|> Stream.map(&String.trim_trailing/1)
|> Stream.map(fn line ->
   Regex.run(~r/^move (\d+) from (\d) to (\d)$/, line, capture: :all_but_first)
   |> Enum.map(&String.to_integer/1)
end)
|> Enum.reduce(stacks, fn [n,src,dst], stacks ->
  crates = Enum.take(stacks[src], n)
  stacks
  |> update_in([src], fn lst -> Enum.drop(lst, n) end) # remove the crates from the source stack
  |> update_in([dst], fn nil -> crates ; lst -> crates ++ lst end) # put the crates in the destination stack
end)
# |> IO.inspect(label: "final stacks")
# transform the map of stacks into a list of stacks in the right order
|> Enum.sort_by(fn {pos, _lst} -> pos end)
# keep only the top crate
|> Enum.map(fn {_pos, [crate | _]} -> crate end)
|> Enum.join("")
|> tap(fn crates -> IO.puts "Crates on top of each stack: #{crates}" end)

elapsed = System.monotonic_time(:microsecond) - start
IO.puts "Job done in #{elapsed} µs"
1 Like

I put each stack of crates in a map, indexed by the stacks index so I could access them without traversing the list of stacks all the time :slight_smile:

Thanks for pointing me to the Access module, there are indeed some neat stuff in there that I didn’t know about.

I implemented my own Stack module that just wraps List with some (hopefully) more stack-ish names. I haven’t decided if this was overkill or not, but I thought it was okay in the end. My solution is just converting the move instructions to stack operations (peek, drop, and push). I didn’t see a cleaner way of doing a pop and not peek and drop, This one might be one I revisit with fresh eyes and consider a major refactor if I think of a cleaner way after reviewing solutions here tomorrow. advent-of-code/advent_of_code_2022.livemd at main · bmitc/advent-of-code · GitHub

Also, I hand-coded the initial stacks because I didn’t want to waste time decoding such terrible input.

(Gotta scroll to see the bulk of the solution.)


defmodule Day5 do
  @moduledoc """
  Solutions for Day 5
  """

  alias Stack

  @typedoc """
  Represents a crate where the name of the crate is an integer index
  """
  @type crate() :: non_neg_integer()

  @typedoc """
  Represents a single stack of crates
  """
  @type stack() :: Stack.t(crate())

  @type stack_name() :: non_neg_integer()

  @typedoc """
  Represents an instruction that says which crate to move from what stack
  to what stack
  """
  @type move_instruction() :: %{
          crate: crate(),
          from: stack_name(),
          to: stack_name()
        }

  @spec initial_stacks() :: [Stack.t()]
  def initial_stacks() do
    [
      ["F", "T", "N", "Z", "M", "G", "H", "J"],
      ["J", "W", "V"],
      ["H", "T", "B", "J", "L", "V", "G"],
      ["L", "V", "D", "C", "N", "J", "P", "B"],
      ["G", "R", "P", "M", "S", "W", "F"],
      ["M", "V", "N", "B", "F", "C", "H", "G"],
      ["R", "M", "G", "H", "D"],
      ["D", "Z", "V", "M", "N", "H"],
      ["H", "F", "N", "G"]
    ]
    |> Enum.map(&Stack.new/1)
  end

  @doc """
  Parse an assignment pair string into a tuple of section assignment ranges

  ## Examples:
    iex> Day4.parse_assignment_pair("2-4,6-8")
    {2..4, 6..8}
  """
  @spec parse_move_instruction(String.t()) :: move_instruction()
  def parse_move_instruction(string) do
    ["move", number, "from", from, "to", to] = String.split(string, " ", trim: true)

    %{
      number_of_crates: String.to_integer(number),
      from: String.to_integer(from) - 1,
      to: String.to_integer(to) - 1
    }
  end

  @doc """
  List of all move instructions
  """
  @spec move_instructions() :: [move_instruction()]
  def move_instructions() do
    Utilities.read_data(5)
    |> Enum.map(&parse_move_instruction/1)
  end

  @doc """
  Handle a move instruction for the case where crates are moved one at a time
  """
  @spec handle_move_instruction([stack()], move_instruction()) :: [stack()]
  def handle_move_instruction(stacks, move_instruction) do
    # Pop (really peek and drop) and push crates
    1..move_instruction.number_of_crates
    |> Enum.reduce(stacks, fn _, stacks ->
      # Get the crate that will be moved by peeking it from the "from" stack
      crate_to_move =
        stacks
        |> Enum.at(move_instruction.from)
        |> Stack.peek()

      # Drop the crate from the "from" stack and push it to the "to" stack
      stacks
      |> List.update_at(move_instruction.from, &Stack.drop/1)
      |> List.update_at(move_instruction.to, &Stack.push(&1, crate_to_move))
    end)
  end


  @doc """
  Handle a move instruction for the case where multiple crates are moved at once
  """
  @spec handle_move_instruction_in_order([stack()], move_instruction()) :: [stack()]
  def handle_move_instruction_in_order(stacks, move_instruction) do
    # Pop (really peek and drop) and push crates
    crates_to_move =
      stacks
      |> Enum.at(move_instruction.from)
      |> Stack.peek(move_instruction.number_of_crates)

    stacks
    |> List.update_at(
      move_instruction.from,
      &Stack.drop(&1, move_instruction.number_of_crates)
    )
    |> List.update_at(move_instruction.to, &Stack.push_as_one(&1, crates_to_move))
  end

  def part_one() do
    move_instructions()
    |> Enum.reduce(initial_stacks(), fn move_instruction, stacks ->
      handle_move_instruction(stacks, move_instruction)
    end)
    # Peek the top of all the final stacks
    |> Enum.map(&Stack.peek/1)
    |> Enum.join()
  end

  def part_two() do
    move_instructions()
    |> Enum.reduce(initial_stacks(), fn move_instruction, stacks ->
      handle_move_instruction_in_order(stacks, move_instruction)
    end)
    # Peek the top of all the final stacks
    |> Enum.map(&Stack.peek/1)
    |> Enum.join()
  end
end
1 Like

Similar approach to others. Think I just organized parsing/solving a bit different.
I end up committing regex for text splitting, but commented non-regex options (e.g. patterned matched the moves originally)

I wondered the same thing about simplifying peek/drop/push.

I ended up doing something like this...
crates = # current state: %{column_idx => [crate]}
[count, from, to] = # parsed: "move <count> from <from> to <to>"
{to_move, crates} = Map.get_and_update(crates, from, &Enum.split(&1, count))
# Reverse is for part 1 only, remove for part 2
Map.update(crates, to, [], &(Enum.reverse(to_move) ++ &1))

Lenses in general aren’t really popular outside of the “hard core FP”.

My solution:

1 Like

I didn’t enjoy the parsing process. Here’s my code for it:

  def read_input(fname) do
    [crates, moves] =
      File.read!("priv/#{fname}")
      |> String.split("\n\n", trim: true)

    {to_map(crates), to_cmdlist(moves)}
  end

  # input -> list of rows
  # each row:
  # - split into list of single-character strings
  # - drop the [ and ] characters
  # - filter out "empty" crates
  # - add index to turn it into {crate, column} tuples
  # use flatmap to turn it into a map
  # - the key is the column (1, 2, ...)
  # - value is a list of crates in that column 
  defp to_map(crates) do
    String.split(crates, "\n", trim: true)
    |> Enum.flat_map(fn row ->
      String.graphemes(row)
      |> Enum.drop_every(2)
      |> Enum.with_index()
      |> Enum.filter(fn {crate, _} -> crate != " " end)
    end)
    |> Enum.reverse()
    |> Enum.reduce(%{}, fn {crate, col}, cratesmap ->
      Map.update(cratesmap, div(col, 2) + 1, [crate], fn existing -> [crate | existing] end)
    end)
  end

  defp to_cmdlist(moves) do
    moves
    |> String.split("\n", trim: true)
    |> Enum.map(fn movestr ->
      [_, count, _, from, _, to] = String.split(movestr, " ", trim: true)
      {String.to_integer(count), String.to_integer(from), String.to_integer(to)}
    end)
  end

Moving the crates was more work than necessary:

  def part1({crates, commands}) do
    Enum.reduce(commands, crates, fn {count, from, to}, crates ->
      {take, remain} = Enum.split(crates[from], count)

      Map.put(crates, from, remain)
      |> Map.put(to, Enum.reverse(take) ++ crates[to])
    end)
    |> top_crates()
  end

  def part2({crates, commands}) do
    Enum.reduce(commands, crates, fn {count, from, to}, crates ->
      {take, remain} = Enum.split(crates[from], count)

      Map.put(crates, from, remain)
      |> Map.put(to, take ++ crates[to])
    end)
    |> top_crates()
  end

  defp top_crates(crates) do
    Map.keys(crates)
    |> Enum.sort()
    |> Enum.map(fn col -> hd(crates[col]) end)
    |> Enum.join("")
  end
1 Like

I spent much more time parsing the input than moving the crates :sweat_smile:

defmodule Day05 do
  def part1(input_path) do
    {stacks, instructions} = parse_input(input_path)

    instructions
    |> Enum.reduce(stacks, fn instruction, stacks ->
      move1(stacks, instruction.from, instruction.to, instruction.amount)
    end)
    |> Enum.sort()
    |> Enum.map(&elem(&1, 1))
    |> Enum.map(&hd/1)
  end

  def part2(input_path) do
    {stacks, instructions} = parse_input(input_path)

    instructions
    |> Enum.reduce(stacks, fn instruction, stacks ->
      move2(stacks, instruction.from, instruction.to, instruction.amount)
    end)
    |> Enum.sort()
    |> Enum.map(&elem(&1, 1))
    |> Enum.map(&hd/1)
  end

  defp parse_input(input_path) do
    [stacks_part, instructions_part] =
      input_path
      |> File.read!()
      |> String.split("\n\n", parts: 2, trim: true)

    {build_stacks(stacks_part), build_instructions(instructions_part)}
  end

  defp build_stacks(stacks_part) do
    stacks_part
    |> String.split("\n", trim: true)
    |> Enum.reverse()
    |> tl()
    |> Enum.map(&to_layer(&1, []))
    |> Enum.reduce(%{}, &push_layer/2)
  end

  defp to_layer("", acc), do: Enum.reverse(acc)
  defp to_layer("    " <> rest, acc), do: to_layer(rest, [?_ | acc])
  defp to_layer("   " <> rest, acc), do: to_layer(rest, [?_ | acc])
  defp to_layer(<<?[, char, ?], ?\s, rest::binary>>, acc), do: to_layer(rest, [char | acc])
  defp to_layer(<<?[, char, ?], rest::binary>>, acc), do: to_layer(rest, [char | acc])

  defp push_layer(layer, stacks) do
    layer
    |> Enum.with_index(1)
    |> Enum.reduce(stacks, fn
      {?_, _}, stacks -> stacks
      {char, i}, stacks -> Map.update(stacks, i, [char], &[char | &1])
    end)
  end

  defp build_instructions(instructions_part) do
    ~r/\d+/m
    |> Regex.scan(instructions_part)
    |> List.flatten()
    |> Enum.map(&String.to_integer/1)
    |> Enum.chunk_every(3)
    |> Enum.map(fn [amount, from, to] ->
      %{amount: amount, from: from, to: to}
    end)
  end

  defp move1(stacks, from, to, amount) do
    {from_stack, to_stack} = for _ <- 1..amount, reduce: {stacks[from], stacks[to]} do
      {[h | t], to_stack} -> {t, [h | to_stack]}
    end

    %{stacks | from => from_stack, to => to_stack}
  end

  defp move2(stacks, from, to, amount) do
    {to_move, from_stack} = Enum.split(stacks[from], amount)
    to_stack = to_move ++ stacks[to]

    %{stacks | from => from_stack, to => to_stack}
  end
end

I certainly overengieneered this, as I created a struct for holding the state and having an API to call, transforming from and to text (for debugging). But that also meant part 2 was actually just taking away from the existing solution :smiley:

Solution
defmodule Day5 do
  defmodule State do
    defstruct stacks: %{}

    @col_size 3

    def from_text(text) do
      lines = String.split(text, "\n")
      {labels_line, stack_lines} = List.pop_at(lines, -1)
      labels = parse_columns(" ", labels_line)
      stack_columns = Enum.map(stack_lines, &parse_columns([" ", "[", "]"], &1))

      stacks =
        stack_columns
        |> Enum.reverse()
        |> Enum.reduce(Enum.map(labels, &{&1, []}), fn line, stack ->
          Enum.zip(stack, line)
          |> Enum.map(fn
            {col, ""} -> col
            {{label, stack}, cargo} -> {label, [cargo | stack]}
          end)
        end)

      %__MODULE__{stacks: Map.new(stacks)}
    end

    defp parse_columns(trim, <<text::binary-size(@col_size)>>) do
      [trim(text, trim)]
    end

    defp parse_columns(trim, <<text::binary-size(@col_size), " ", rest::binary>>) do
      [trim(text, trim) | parse_columns(trim, rest)]
    end

    defp trim(string, to_trim) do
      to_trim
      |> List.wrap()
      |> Enum.reduce(string, &String.trim(&2, &1))
    end

    def move(%__MODULE__{stacks: stacks}, from, to, num) do
      {to_move, stacks} =
        Map.get_and_update!(stacks, from, fn list ->
          Enum.split(list, num)
        end)

      to_put = Enum.reverse(to_move)

      stacks = Map.update!(stacks, to, fn current -> to_put ++ current end)
      %__MODULE__{stacks: stacks}
    end

    def move_alternate(%__MODULE__{stacks: stacks}, from, to, num) do
      {to_move, stacks} =
        Map.get_and_update!(stacks, from, fn list ->
          Enum.split(list, num)
        end)

      stacks = Map.update!(stacks, to, fn current -> to_move ++ current end)
      %__MODULE__{stacks: stacks}
    end

    def id(%__MODULE__{stacks: stacks}) do
      stacks
      |> Map.values()
      |> Enum.map_join("", &List.first/1)
    end

    defimpl Inspect do
      def inspect(%_{stacks: stacks}, _) do
        stack_lists = Map.values(stacks)
        height = stack_lists |> Enum.map(&length/1) |> Enum.max()
        indexes = -height..-1//1

        lines =
          Enum.map_join(indexes, "\n", fn index ->
            Enum.map_join(stack_lists, " ", fn stack ->
              case Enum.at(stack, index) do
                nil -> "   "
                cargo -> "[#{cargo}]"
              end
            end)
          end)

        labels = stacks |> Map.keys() |> Enum.map_join(" ", fn label -> " #{label} " end)

        text = lines <> "\n" <> labels

        """
        Day5.State.from_text(\"\"\"
        #{text}
        \"\"\")
        """
      end
    end
  end

  def parse(text) do
    [starting_stacks, procedure] = String.split(text, "\n\n", parts: 2)
    state = State.from_text(starting_stacks)

    operations =
      ~r/^move (\d+) from (.) to (.)$/m
      |> Regex.scan(procedure)
      |> Enum.map(fn [_, num, from, to] ->
        %{num: String.to_integer(num), from: from, to: to}
      end)

    {state, operations}
  end

  def run_operations(text) do
    {state, operations} = parse(text)

    operations
    |> Enum.reduce(state, fn operation, state ->
      State.move(state, operation.from, operation.to, operation.num)
    end)
    |> State.id()
  end

  def run_operations_alternate(text) do
    {state, operations} = parse(text)

    operations
    |> Enum.reduce(state, fn operation, state ->
      State.move_alternate(state, operation.from, operation.to, operation.num)
    end)
    |> State.id()
  end
end
1 Like

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 spent hours trying to figure out how to do that, only to end up with a bunch of hard to understand Enum.reduce.
Going to study the other answers in this thread for sure!

Day5
the_crates = [
  ["P", "Z", "M", "T", "R", "C", "N"],
  ["Z", "B", "S", "T", "N", "D"],
  ["G", "T", "C", "F", "R", "Q", "H", "M"],
  ["Z", "R", "G"],
  ["H", "R", "N", "Z"],
  ["D", "L", "Z", "P", "W", "S", "H", "F"],
  ["M", "G", "C", "R", "Z", "D", "W"],
  ["Q", "Z", "W", "H", "L", "F", "J", "S"],
  ["N", "W", "P", "Q", "S"]
]

defmodule Day5 do
  def move_crates(crates, quantity, from, to) do
    from = from - 1
    to = to - 1
    {crates_to_move, remaining} = Enum.at(crates, from)  |> Enum.split(quantity)
    crates
    |> Enum.with_index
    |> Enum.reduce([], fn {stack, index}, acc ->
      case index do
        ^from -> acc ++ [remaining]
        # part 1
        ^to -> acc ++ [Enum.concat(Enum.reverse(crates_to_move), stack)]
        # part 2
        #^to -> acc ++ [Enum.concat(crates_to_move, stack)]
        _ -> acc ++ [stack]
       end
    end)
  end
end


File.stream!("input05")
|> Stream.map(fn line ->
     String.split(line, ["move","from"," to"], trim: true)
     |> Enum.map(&String.trim/1)
     |> Enum.map(&String.to_integer/1)
   end)
|> Enum.reduce(the_crates, fn [quantity, from, to], acc ->
     Day5.move_crates(acc, quantity, from, to)
    end)
|> Enum.map(&hd/1)
|> Enum.join("")
|> IO.puts
1 Like

One more linear binary scanning day

defmodule AOC do

  import :erlang, only: [setelement: 3, element: 2]

  def run(input) do
    {towers, commands_str} = traverse_towers(input)
    IO.inspect(towers)
    towers = traverse_commands(commands_str, towers)
    to_string Enum.map(Tuple.to_list(towers), fn [head | _] -> head end)
  end

  defp index_for(?\s, index), do: index + 1
  defp index_for(?\n, index), do: 1

  def traverse_towers(string, index \\ 1, towers \\ {}) do
    case string do
      <<"\nmove ", tail :: binary>> ->
        {towers, tail}

      <<"   ", delimiter, tail :: binary>> ->
        towers =
          with towers when tuple_size(towers) < index <- towers do
            Tuple.append(towers, [])
          end
        traverse_towers(tail, index_for(delimiter, index), towers)

      <<?[, char, ?], delimiter, tail :: binary>> ->
        towers =
          case towers do
            towers when tuple_size(towers) < index ->
              Tuple.append(towers, [char])

            _ ->
              tower = element(index, towers)
              setelement(index, towers, [char | tower])
          end

        traverse_towers(tail, index_for(delimiter, index), towers)

      <<" ", index_char, " ", _, tail :: binary>> when index_char in ?0..?9 ->
        tower = element(index, towers)
        tower = :lists.reverse(tower)
        towers = setelement(index, towers, tower)
        traverse_towers(tail, index + 1, towers)
    end
  end

  defp traverse_commands(string, towers, number \\ 0, acc \\ [])
  defp traverse_commands("", towers, 0, [to, from, how_much]) do
    move(towers, how_much, from, to)
  end
  defp traverse_commands(string, towers, 0, [to, from, how_much] = acc) do
    towers = move(towers, how_much, from, to)
    traverse_commands(string, towers, 0, [])
  end
  defp traverse_commands(string, towers, number, acc) do
    case string do
      <<"\nmove ", tail :: binary>> ->
        traverse_commands(tail, towers, 0, [number | acc])

      <<" from ", tail :: binary>> ->
        traverse_commands(tail, towers, 0, [number | acc])

      <<" to ", tail :: binary>> ->
        traverse_commands(tail, towers, 0, [number | acc])

      "\n" ->
        traverse_commands("", towers, 0, [number | acc])

      <<char, tail :: binary>> ->
        # IO.inspect <<char>>, label: :char
        traverse_commands(tail, towers, number * 10 + char - ?0, acc)

      "" ->
        traverse_commands("", towers, 0, [number | acc])
    end
  end

  defp move(towers, how_much, from_index, to_index) do
    to = element(to_index, towers)
    from = element(from_index, towers)
    {moving, from} = :lists..split(how_much, from)
    to = moving ++ to

    towers = setelement(to_index, towers, to)
    setelement(from_index, towers, from)
  end

end

IO.inspect AOC.run IO.read :eof

And it is also extremely unobvious and unsupported feature. Performance, structures, lenses as lists, introspection, Access.key etc.