Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

# Advent of Code 2022 - Day 5 ```elixir Mix.install([ {:explorer, "~> 0.4.0"}, {:req_aoc, github: "mcrumm/req_aoc", ref: "eef58c9"} ]) ``` ## Puzzle description [Day 5: Supply Sacks](https://adventofcode.com/2022/day/5). ## Solution ```elixir defmodule Part1 do # preflight: ## split string into lines (without break characters) def run(input) do ### # for reference: # the first set of rows are the crates, for example: [A] [B] [C] # followed by exactly one row of stack IDs (1 2 3) # followed by an empty row ("") # the next set of rows are the movement instructions, ex: move 1 from 2 to 1 ### # idea: # Split the input on the empty row # Parse all of the crate rows into lists of characters # **bad idea, we lose column information for the crate** ### # idea 2: # split the input on the empty row # for the first half, split each line into 4-byte chunks # parse each chunk as either nil, A-Z, or 0-9 # drop the last line (or use it to validate the row lengths) {crates, moves} = new(input) moves |> Enum.reduce(crates, fn x, acc -> move(acc, x) end) |> answer() end @regex ~r/[A-Z]/ def new(input) do split_index = Enum.find_index(input, fn "" -> true _ -> false end) {crates_strings, ["" | move_strings]} = Enum.split(input, split_index) crates = crates_strings |> to_crates() moves = move_strings |> to_moves() {crates, moves} end defp to_crates(crates_with_labels) do crates = crates_with_labels |> Enum.reverse() |> tl() |> Enum.reverse() |> Enum.reduce([], fn line, acc -> chunks = line |> chunk(4) |> Enum.map(fn chunk -> @regex |> Regex.scan(chunk) |> List.first() |> case do nil -> nil # [bin] when is_binary(bin) -> :binary.first(bin) [bin] -> bin end end) [chunks | acc] end) |> Enum.reverse() |> crates_df() end defp crates_df(crates) do crates |> Enum.with_index() |> Enum.reduce([], fn {moves, idx}, acc -> [{to_string(idx), moves} | acc] end) |> Enum.reverse() |> Explorer.DataFrame.new() |> rotate_df() end @regex ~r/[0-9]/ def to_moves(move_strings) do move_strings |> Enum.map(fn move -> @regex |> Regex.scan(move) |> Enum.map(fn [m] -> String.to_integer(m) end) end) end @doc """ Updates the crate positions by the given moves. """ def move({%Explorer.DataFrame{} = crates, moves}) when is_list(moves) and length(moves) > 0 do [move | rest] = moves new_crates = move(crates, move) {new_crates, rest} end defp move(crates, [count, from, to]) do [from, to] = for x <- [from, to], do: to_string(x) # what `move/2` needs to do: # - first, assume all moves are legal # - find the "from" column # - take "count" items # - put them on the "to" column # - account for nils, somehow Explorer.DataFrame.mutate_with(crates, fn df -> [ {from, Explorer.Series.from_list([])}, {to, Explorer.Series.from_list([])} ] end) end @doc """ Returns the answer string for the given crate dataframe. Converts the dataframe back to columns of rows. """ def answer(%Explorer.DataFrame{} = crate_df) do crate_df |> rotate_df() |> Explorer.DataFrame.to_series() |> Map.values() |> Explorer.Series.coalesce() |> Explorer.Series.to_list() |> Enum.join() end @doc """ Rotates and converts the dataframe for easier reading. """ def preview({%Explorer.DataFrame{} = crates, _}) do preview(crates) end def preview(%Explorer.DataFrame{} = crates) do crates |> rotate_df() |> Explorer.DataFrame.to_columns() end # i am sure there must be a better way to do this, but today i don't know what that way is. # so this is a really long-winded and probably inefficient way to rotate the crates matrix # so we can work with columnar data. defp rotate_df(%Explorer.DataFrame{} = df) do df |> Explorer.DataFrame.to_rows() |> Enum.map(fn map -> Map.values(map) end) |> Enum.with_index() |> Enum.reduce([], fn {series, idx}, acc -> [{to_string(idx), series} | acc] end) |> Enum.reverse() |> Explorer.DataFrame.new() end @doc """ Splits a string into n-byte chunks, preserving leftovers. h/t @cmkarlsson - https://elixirforum.com/t/how-to-split-string-into-multiple-chunks-by-size/39662/6 """ def chunk(string, size), do: chunk(string, size, []) defp chunk(<<>>, size, acc), do: Enum.reverse(acc) defp chunk(string, size, acc) when byte_size(string) > size do <<c::size(size)-binary, rest::binary>> = string chunk(rest, size, [c | acc]) end defp chunk(leftover, size, acc) do chunk(<<>>, size, [leftover | acc]) end end defmodule Part2 do # preflight: ## split string into lines (without break characters) def run(input) do ### input end end ExUnit.start(autorun: false) defmodule Test do use ExUnit.Case, async: true @example_input ~s( [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 ) year_day = {2022, 05} @input System.fetch_env!("LB_AOC_SESSION") |> ReqAOC.fetch!(year_day, max_retries: 0) test "new/1" do assert {crates, moves} = @example_input |> to_lines() |> Part1.new() assert Part1.preview(crates) == %{ "0" => [nil, "D", nil], "1" => ["N", "C", nil], "2" => ["Z", "M", "P"] } assert moves == [ [1, 2, 1], [3, 1, 3], [2, 2, 1], [1, 1, 2] ] end test "move/1" do crates_moves = @example_input |> to_lines() |> Part1.new() assert {crates, [_, _, _]} = next = Part1.move(crates_moves) assert Part1.preview(crates) == %{ "0" => ["D", nil, nil], "1" => ["N", "C", nil], "2" => ["Z", "M", "P"] } assert {crates, [_, _]} = Part1.move(next) assert Explorer.DataFrame.to_columns(crates) == %{ "0" => [], "1" => [nil, "C", "M"], "2" => ["Z", "N", "D", "P"] } assert {crates, [_]} = Part1.move(next) assert Explorer.DataFrame.to_columns(crates) == %{ "0" => ["M", "C"], "1" => [nil], "2" => ["Z", "N", "D", "P"] } assert {crates, []} = Part1.move(next) assert Explorer.DataFrame.to_columns(crates) == %{ "0" => ["M"], "1" => [nil, "C"], "2" => ["Z", "N", "D", "P"] } end test "part 1" do assert @example_input |> to_lines() |> Part1.run() == "CMZ" @input |> to_lines() |> Part1.run() |> dbg() end test "part 2" do assert @example_input |> to_lines() |> Part2.run() == nil @input |> to_lines() |> Part2.run() |> dbg() end @doc """ Splits a string by newlines. """ def to_lines(input) when is_binary(input) do input |> String.trim("\n") |> String.split("\n") end end ExUnit.configure(trace: true) ExUnit.run() ```
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run on your machine

with Livebook Desktop

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More ×