File.read!("input.txt")
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.
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!
late to the party… I hate my solution already after seeing some of the more elegant ways here
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.