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.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.

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.

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)
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 :heart: 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 :slight_smile:

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 :sweat_smile:
Want this
grafik
but after saving
grafik

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 :slightly_smiling_face:!)

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. :grimacing: :sweat_smile:

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 :slight_smile:

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