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.trim_leading("move ")
|> 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.
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.
Yes! Let me add one ad:
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)
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
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"]]
instead of
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)
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