# Advent of Code 2022 - Day 5

``````defmodule Day5 do
def input do
[stacks, instructions] = File.read!("input5.txt") |> String.split("\n\n", trim: true)

stacks = map_stacks(stacks)
instructions = map_instructions(instructions)

[stacks, instructions]
end

def part1 do
[stacks, instructions] = input()

Enum.reduce(instructions, stacks, fn [count, from, to] , acc ->
to_move = Enum.at(acc, from - 1) |> Enum.take(count) |> Enum.reverse()

List.update_at(acc, from - 1, fn x -> Enum.drop(x, count) end)
|> List.update_at(to - 1, fn x -> to_move ++ x end)
end)
|> Enum.map(&List.first/1)
|> Enum.join("")
end

def part2 do
[stacks, instructions] = input()

Enum.reduce(instructions, stacks, fn [count, from, to] , acc ->
to_move = Enum.at(acc, from - 1) |> Enum.take(count)

List.update_at(acc, from - 1, fn x -> Enum.drop(x, count) end)
|> List.update_at(to - 1, fn x -> to_move ++ x end)
end)
|> Enum.map(&List.first/1)
|> Enum.join("")
end

@doc """
Maps the given rows of stacks to columns, and removes the numbers

## Example

iex> Day5.map_stacks("   [D]    \\n[N] [C]    \\n[Z] [M] [P]\\n 1   2   3 ")
[["N", "Z"], ["D", "C", "M"], ["P"]]
"""
def map_stacks(stacks) do
stacks
|> String.split("\n", trim: true)
|> Enum.map(fn row ->
row
|> String.split("", trim: true)
|> Enum.chunk_every(4)
|> Enum.map(fn item -> Enum.take(item, 3) |> Enum.at(1) end)
end)
|> Enum.reverse()
|> Enum.drop(1)
|> Enum.reverse()
|> Enum.zip_with(& &1)
|> Enum.map(&Enum.reject(&1, fn x -> x == " " end))
end

@doc """
Maps the instructions to a list of numbers [count, from, to]

## Example

iex> Day5.map_instructions("move 1 from 2 to 1\\nmove 3 from 1 to 3\\nmove 2 from 2 to 1\\nmove 1 from 1 to 2\\n")
[[1, 2, 1], [3, 1, 3], [2, 2, 1], [1, 1, 2]]
"""
def map_instructions(instructions) do
instructions
|> String.split("\n", trim: true)
|> Enum.map(fn instruction ->
instruction
|> String.replace("from ", "")
|> String.replace("to ", "")
|> String.split(" ")
|> Enum.map(&String.to_integer/1)
end)
end
end

``````

I could not disagree more.

Oh, wait, I could.

`Access` is something I use in each and every project, I even created the library to inject a performant `Access` implementation into home-baked structs (`estructura`.) Also anyone who dealt with deeply nested structures at least once, would use it again and again for its simplicity, elegance and uniformness.

2 Likes

Yeah, I’ve seen this library, and it is a nice idea, but it doesn’t work for already present structures. For example, Access could be useful for `Plug.Conn`, but it is unimplemented for it. And reason behind this is purely historical, because Access was meant to be a protocol, but it was changed to behaviour because of performance.

On the other hand, do we even need to define getters and setters per-structure? I’d suggest using the different approach. Instead of defining getters and setters per-structure, we can simply define lenses for every structure or key we need.

I’ve tried this idea in multiple projects and it worked really well. Take a look at pathex, it does everything `Access` can, but with performance, more functional style and richer set of functions.

The first question about Access I get is “why can’t I get a value from structure” and the second one is “how do I separate `nil` from not found value”. From this moment, everyone just stops using this module and `Access.key` doesn’t help here.

Now here for you. New and shiny. Added with 1.14… Access.slice!!!

``````    Enum.reduce(instructions, stacks, fn [amount, from, to], stacks ->
{crates, stacks} = pop_in(stacks, [from, Access.slice(0..(amount - 1))])
Map.update(stacks, to, crates, &place_crates_on_stack(model, crates, &1))
end)
``````
2 Likes

Yes, I saw it and it looks great. I am still fine with an stdlib `Access` though.

You don’t. That’s the thing. If you need to separate `nil` from no value, you are abusing `nil` in your code. `nil` semantically stays for “no value.” If you need something else, use `:undefined`, or `:none`, or like. `nil` is not something one wants to distinguish from “none.” Otherwise we must complain that in non-strict boolean operators it acts as `false` (`nil && 42`.)

I absolutely this.

I got a json from external service, and I need to know if the field is present in the structure or not
I go `get_in ["users", name, "middlename"]`, and I don’t know if the user just forgot to provide the middle name, or user just has no middlename.

if the user just forgot to provide the middle name, or user just has no middlename

There is no difference from the point of view of your application. No difference. None.

Also, asking for user name, as well as for their address must be a single string field. Otherwise, sooner or later you’ll end up with the user who cannot enter their data.

But this is an edge case, it rarely happens and I understand this maybe-style design behind Access. However, I prefer for edge-cases to be covered, to not go inventing a tool each time an edge case occurs. The same problem with `Repo.get` simply drives me crazy

I thought about Access but I thought, “piles of crates with an index” sounds like a map!

Just throwing mine into the mix.

``````  defp move_9000(0, from, to), do: {from, to}
defp move_9000(number, [crate | from], to), do: move_9000(number - 1, from, [crate | to])

defp move_9001(number, from, to) do
{moving, moved_from} = Enum.split(from, number)
{moved_from, moving ++ to}
end
``````

Completing in <100μs so can’t be too bad

3 Likes

I found it really hard to read the input data. Ended up with code similar to some others that split the characters and chunk them.

But what really confused me and “annoyed” me is the following thing that i can’t figure out: I usually have the example input from the website in a module attribute like

``````defmodule AOC2022.Day5Test do
use ExUnit.Case

alias AOC2022.Day5, as: Day
doctest Day

@example_input """
[D]
[N] [C]
[Z] [M] [P]
1   2   3
"""

# ...
end
``````

and it’s not shown here but when saving the file it removes the trailing whitespaces from the multiline string, thus making my input “wrong”. Can i somehow stop that from happening?

Edit: Screenshots to the rescue
Want this

but after saving

what editor/IDE setup are you using?
also, in this particular case, how would the trailing whitespace characters affect your parsing of the input string?

I’m using VS Code. My test case looks / looked like this:

``````test "parse_stacks 1b" do
input = """
[D]
[N] [C]
[Z] [M] [P]
1   2   3
"""

actual = Day.parse_stacks(input)

expected = [
["N", "Z"],
["D", "C", "M"],
["P"]
]

assert actual == expected
end
``````

and my implementation is

``````def parse_stacks(input) do
input
|> String.split("\n", trim: true)
|> Enum.reverse()
|> Enum.drop(1)
|> Enum.map(&String.graphemes/1)
|> Enum.map(&Enum.chunk_every(&1, 4))
|> Enum.map(&Enum.map(&1, fn chunks -> Enum.join(chunks) end))
|> Enum.zip()
|> Enum.map(&Tuple.to_list/1)
|> Enum.map(&Enum.reverse/1)
|> Enum.map(&to_proper_names/1)
|> dbg()
end
``````

as you can see i placed a `dbg` there for now to show where the problem is because when zipping it’s missing the last stack because the lists do not have enough elements:

``````input #=> "    [D]\n[N] [C]\n[Z] [M] [P]\n 1   2   3\n"
|> String.split("\n", trim: true) #=> ["    [D]", "[N] [C]", "[Z] [M] [P]", " 1   2   3"]
|> Enum.reverse() #=> [" 1   2   3", "[Z] [M] [P]", "[N] [C]", "    [D]"]
|> Enum.drop(1) #=> ["[Z] [M] [P]", "[N] [C]", "    [D]"]
|> Enum.map(&String.graphemes/1) #=> [
["[", "Z", "]", " ", "[", "M", "]", " ", "[", "P", "]"],
["[", "N", "]", " ", "[", "C", "]"],
[" ", " ", " ", " ", "[", "D", "]"]
]
|> Enum.map(&Enum.chunk_every(&1, 4)) #=> [
[["[", "Z", "]", " "], ["[", "M", "]", " "], ["[", "P", "]"]],
[["[", "N", "]", " "], ["[", "C", "]"]],
[[" ", " ", " ", " "], ["[", "D", "]"]]
]
|> Enum.map(&Enum.map(&1, fn chunks -> Enum.join(chunks) end)) #=> [["[Z] ", "[M] ", "[P]"], ["[N] ", "[C]"], ["    ", "[D]"]]
|> Enum.zip() #=> [{"[Z] ", "[N] ", "    "}, {"[M] ", "[C]", "[D]"}]
|> Enum.map(&Tuple.to_list/1) #=> [["[Z] ", "[N] ", "    "], ["[M] ", "[C]", "[D]"]]
|> Enum.map(&Enum.reverse/1) #=> [["    ", "[N] ", "[Z] "], ["[D]", "[C]", "[M] "]]
|> Enum.map(&to_proper_names/1) #=> [["N", "Z"], ["D", "C", "M"]]
``````

(And thanks for the help !)

Technically, once we know the length is 3, we might do

``````Enum.reduce(input, ..., fn
<<?[, c1, ?], ?\s, ?[, c2, ?], ?\s, ?[, c3, ?]>> -> [c1, c2, c3]
end)
``````

This might be generated, and it might be generated for different numbers of crates (say, up to 10 plus a slow fallback with `String.split/2`.)

Yeah, that’s basically how I did it with `Enum.take_every(4)`. Not sure if that addresses the issue @IloSophiep is having. I think the issue they are having has to do with the `Enum.chunk_every(&1, 4)` call. If instead they used `Enum.drop(&1, 1) |> Enum.take_every(4)` without the join, then I think the zip would be accurate.

I tried to change my code to use what you suggested, but even then i get to the point where my list for the stacks end up with different sizes. Basically i end up with

``````after_take_every = [["Z", "M", "P"], ["N", "C"], ["", "D"]]
``````

``````after_take_every = [["Z", "M", "P"], ["N", "C", ""], ["", "D", ""]]
``````

so my `Enum.zip/1) leaves out the last stack, because my input string does not have the trailing whitespaces. My workaround was writing

``````input =
"" <>
"    [D]    \n" <>
"[N] [C]    \n" <>
"[Z] [M] [P]\n" <>
" 1   2   3 \n"
``````

and it works… - i just figured there might be some smart way that i’m missing.

Yeah, I don’t guess there’s a better way to handle that. I don’t use VSCode but it seems likely that there is some autoformat setting that is removing trailing whitespace.

Parsing the stacks/crates was the difficult part here. I was sure that I will find out here that there’s some clever solution for that, but it looks like there’s not.

I used something like this to get a crate from a line for a specific stack:

``````grapheme_position = fn
1 -> 1
pos -> (pos - 1) * 4 + 1
end

grapheme_position = grapheme_position.(stack_index)
crate = String.at(line, grapheme_position)
``````

My solution in LiveBook

If you are using the ElixirLS extension, it might be the Elixir formatter removing the spaces. But VS Code may be doing it as well, although I would have guessed that this is a setting that needs to be opted into.

I personally just hand coded the stacks part of the input, as I didn’t have the patience to parse such a poor format. I hope it’s not a sign of things to come where parsing gets more and more annoying. In this case, the stacks could have been listed horizontally instead.

Attempt at a compact but straightforward code. Parsing included

``````defmodule Day5 do

def run(callback) do
# split the input in the lines for the initial state, and the moves
[state_str, moves_str] = File.read!("input") |> String.split("\n\n", trim: true)

# split the lines for the initial state stacks, drop the last one
state_lines = state_str |> String.split("\n") |> Enum.drop(-1)

# compute how many stacks we have (divide length of one line by 4)
stacks_count = (String.length(hd(state_lines)) + 1) / 4 |> trunc()

# reduce the states lines in a Map, key = stack id, value = list of letters, top at the start
stacks = Enum.reduce(state_lines, %{}, fn line, acc ->
# go over all the positions where we might find a letter in the line string
Enum.reduce((0..stacks_count-1), acc, fn key, acc ->
pos = key*4+1 # compute letter position from the key (stack index)
case String.at(line, pos) do
" "  -> acc # space, no letter, don't add anything to the Map
char -> acc |> Map.update(key, [char], fn stack -> stack ++ [ char ] end) # found a letter, append it in the stack
end
end)
end)
# now, go over the moves lines
stacks = moves_str |> String.split("\n") |> Enum.reduce(stacks, fn line, acc ->
# parse the line string, get how much should be moved from where to where
[count, from, to] = ~r/move (\d+) from (\d+) to (\d+)/
|> Regex.run(line, capture: :all_but_first)
|> Enum.map(&String.to_integer/1)
# take from one stack to the other, reversing on the go if needed
{to_move, acc} = acc |> Map.get_and_update!(from-1, & Enum.split(&1, count) )
acc |> Map.update!(to-1, & callback.(to_move) ++ &1)
end)
stacks |> Map.values() |> Enum.map_join(&hd/1) |> IO.inspect()
end
end

Day5.run(&Enum.reverse/1) # part 1, reverse when moving over
Day5.run(& &1)            # part 2, don't reverse
``````