jzimbel's recent activity
-
Comment on Google released a .zip web domain and people can't decide if it's the phishing apocalypse or just as bad as any other dodgy link in ~comp
-
Comment on Google released a .zip web domain and people can't decide if it's the phishing apocalypse or just as bad as any other dodgy link in ~comp
jzimbel It’s just common sense to not have domains that share names with common file extensions. .jpg, .pdf, .doc(x), .mp3 all don’t exist because they would have similar use cases—confusing and...It’s just common sense to not have domains that share names with common file extensions. .jpg, .pdf, .doc(x), .mp3 all don’t exist because they would have similar use cases—confusing and unnecessary at best, disastrously malicious at worst.
The fact that they didn’t exist before this, despite the hundreds of other silly TLDs, is evidence that someone considering the addition used basic critical thinking and realized: “Wait, that’s a terrible idea.” Somehow that didn’t happen this time at Google.
-
Comment on My company offers a stipend of $150/quarter to improve my home office in ~life
jzimbel Overall, I would avoid any plant that has special humidity requirements, like ferns and crotons. Succulents are alright but have a hard requirement for full sunlight, and often respond extremely...Overall, I would avoid any plant that has special humidity requirements, like ferns and crotons.
Succulents are alright but have a hard requirement for full sunlight, and often respond extremely poorly to overwatering.
I’m a big fan of most trailing viney plants—of which pothos is one. Others include Tradescantia (“wandering Jew/wandering dude”) and certain Peperomia species. All are tolerant of neglect and easy to propagate.
-
Comment on My company offers a stipend of $150/quarter to improve my home office in ~life
jzimbel Seconding pothos—very tolerant of neglect, very attractive, and if it gets a little leggy after a while you can always cut it, root the cuttings in water for a week or two, and replant them to get...Seconding pothos—very tolerant of neglect, very attractive, and if it gets a little leggy after a while you can always cut it, root the cuttings in water for a week or two, and replant them to get a fuller look or even restart completely.
Peace lilies are also great. There are cultivars of all different sizes. They don’t need much light, have nice foliage, will even grow pretty white flowers without a strong scent, and they make it extremely clear when they need water—the leaves droop dramatically and then bounce right back after a good soak.
-
Comment on Day 19: Not Enough Minerals in ~comp.advent_of_code
jzimbel I looked at /r/adventofcode. Solution hints It's BFS or DFS with heuristics to prune the state space that needs to be explored into something of a manageable size. People have solved the puzzle...I looked at /r/adventofcode.
Solution hints
It's BFS or DFS with heuristics to prune the state space that needs to be explored into something of a manageable size.
People have solved the puzzle with all sorts of different heuristics, and it's mostly just trial and error. Many found success by branching on what robot to aim for building next rather than trying to make that decision at every step. It allows you to fast-forward through the steps where nothing gets built.
This does not spark joy; I'll be skipping this one.
-
Comment on Day 19: Not Enough Minerals in ~comp.advent_of_code
jzimbel (edited )LinkSo does anyone have any idea how to solve this one efficiently? This is the first time I've seen zero people on the private leaderboard solve either part within a half day or so. I took a stab at...So does anyone have any idea how to solve this one efficiently? This is the first time I've seen zero people on the private leaderboard solve either part within a half day or so.
I took a stab at it by always building the "highest-value" available mining robot each minute, which got me somewhat close to the expected value on the test input—I got 29 vs the expected 33.
My brute force approach which recursively tries every available action at each minute unsurprisingly is too slow even on the test input, since the number of branches to try is absolutely huge for even 1 blueprint.
Clearly the key is knowing when to hoard resources to build a higher-tier robot earlier, rather than building way too many ore robots, but how?
-
Comment on Day 14: Regolith Reservoir in ~comp.advent_of_code
jzimbel Elixir I went through a lot of iterations on this one. Overall I did a few different combinations of It turned out that the simplest approach was also the fastest: model the data as a flat set,...Elixir
I went through a lot of iterations on this one. Overall I did a few different combinations of
Data structure: a flat set of
{x, y}
tuples OR a map{x => sorted_list(y)}
to make accessing each column easier
Falling logic: drop straight to the next available obstruction OR always drop 1 unit at a timeIt turned out that the simplest approach was also the fastest: model the data as a flat set, and always drop the sand grains one unit at a time. Other approaches introduced more edge cases and also required repeatedly searching through collections (either the column lists, or the entire set of coordinate pairs), which more than canceled out any efficiency gains from skipping steps.
Both parts
I used multiple function clauses indrop_sand/4
to separate out the 3 main checks (:d
,:l
,:r
) and keep the number of nested branches to a minimum.defmodule AdventOfCode.Solution.Year2022.Day14 do @source {500, 0} def part1(input) do input |> stream_states(fn coords, _sand -> coords end) |> Stream.map(&MapSet.size/1) |> Stream.chunk_every(2, 1) |> Enum.find_index(&match?([count, count], &1)) end def part2(input) do input |> stream_states(&MapSet.put(&1, &2)) |> Enum.find_index(&MapSet.member?(&1, @source)) end defp stream_states(input, use_floor) do {coords, floor_y} = parse(input) Stream.iterate(coords, &drop_sand(&1, {floor_y, use_floor})) end defp drop_sand(coords, floor, sand \\ @source, move \\ :d) defp drop_sand(coords, {floor_y, use_floor} = floor, {x, y} = sand, :d) do next = {x, y + 1} cond do MapSet.member?(coords, next) -> drop_sand(coords, floor, sand, :l) y == floor_y - 1 -> use_floor.(coords, sand) true -> drop_sand(coords, floor, next, :d) end end defp drop_sand(coords, floor, {x, y} = sand, :l) do next = {x - 1, y + 1} if MapSet.member?(coords, next), do: drop_sand(coords, floor, sand, :r), else: drop_sand(coords, floor, next, :d) end defp drop_sand(coords, floor, {x, y} = sand, :r) do next = {x + 1, y + 1} if MapSet.member?(coords, next), do: MapSet.put(coords, sand), else: drop_sand(coords, floor, next, :d) end defp parse(input) do coords = input |> String.split("\n", trim: true) |> Enum.map(&parse_path/1) |> Enum.reduce(MapSet.new(), &MapSet.union/2) floor_y = coords |> Enum.max_by(&elem(&1, 1)) |> elem(1) |> Kernel.+(2) {coords, floor_y} end defp parse_path(line) do ~r/(\d+),(\d+)/ |> Regex.scan(line, capture: :all_but_first) |> Enum.chunk_every(2, 1, :discard) |> Enum.flat_map(&parse_stroke/1) |> MapSet.new() end defp parse_stroke([[x1, y1], [x2, y2]]) do for x <- String.to_integer(x1)..String.to_integer(x2), y <- String.to_integer(y1)..String.to_integer(y2), do: {x, y} end end
Part 1 runs in 26 ms, part 2 in 744 ms.
-
Comment on Day 13: Distress Signal in ~comp.advent_of_code
jzimbel Elixir Pattern matching + multiple function clauses helped me separate the different cases out in my recursive in_order?/2 function. The puzzle's comparison method is actually very close to...Elixir
Pattern matching + multiple function clauses helped me separate the different cases out in my recursive
in_order?/2
function. The puzzle's comparison method is actually very close to Elixir/Erlang's default term ordering for lists—elements are compared in order left to right until a "tie-breaker" is found. I almost could have gotten away with simply comparing the lists withl < r
! But the case where you wrap a number in a list when comparing it to another list prevented me from getting to cheat that way, unfortunately. Oh well :)In a similar vein, all atoms except
nil
(aka:nil
) are truthy, so having the comparator return:tie
when two lists are "equal" results in the correct sorting when it's passed toEnum.sort/2
.I used
Code.eval_string/1
to convert the input lines into lists (after doing a basic check that the string is composed only of[
]
,
and digits), which saved some time.Both parts
defmodule AdventOfCode.Solution.Year2022.Day13 do def part1(input) do input |> String.split("\n", trim: true) |> Enum.map(&parse_packet/1) |> Enum.chunk_every(2) |> Enum.with_index(1) |> Enum.filter(fn {[l, r], _i} -> in_order?(l, r) end) |> Enum.map(fn {_pair, i} -> i end) |> Enum.sum() end @divider_packets [[[2]], [[6]]] def part2(input) do input |> String.split("\n", trim: true) |> Enum.map(&parse_packet/1) |> Enum.concat(@divider_packets) |> Enum.sort(&in_order?/2) |> Enum.with_index(1) |> Enum.filter(fn {packet, _i} -> packet in @divider_packets end) |> Enum.map(fn {_packet, i} -> i end) |> Enum.product() end defp parse_packet(line) do if Regex.match?(~r/^[\[\],\d]+$/, line), do: elem(Code.eval_string(line), 0), else: raise("I ain't eval-ing that") end # out of values to compare; should only occur in nested lists defp in_order?([], []), do: :tie # lists are different lengths and we've reached the end of one defp in_order?([], [_ | _]), do: true defp in_order?([_ | _], []), do: false # both values are integers defp in_order?([n | ltail], [n | rtail]) when is_integer(n), do: in_order?(ltail, rtail) defp in_order?([ln | _], [rn | _]) when is_integer(ln) and is_integer(rn), do: ln < rn # both values are lists defp in_order?([ll | ltail], [rl | rtail]) when is_list(ll) and is_list(rl) do case in_order?(ll, rl) do :tie -> in_order?(ltail, rtail) result -> result end end # one value is an integer defp in_order?([n | ltail], r) when is_integer(n), do: in_order?([[n] | ltail], r) defp in_order?(l, [n | rtail]) when is_integer(n), do: in_order?(l, [[n] | rtail]) end
-
Comment on What have you been eating, drinking, and cooking? in ~food
-
Comment on Day 10: Cathode-Ray Tube in ~comp.advent_of_code
jzimbel Elixir Fun with streams! First time I've used Stream.transform/3. A bit tricky, but it made my code to get the signal value at each cycle super concise. Both parts defmodule...Elixir
Fun with streams! First time I've used
Stream.transform/3
. A bit tricky, but it made my code to get the signal value at each cycle super concise.Both parts
defmodule AdventOfCode.Solution.Year2022.Day10 do def part1(input) do input |> String.split("\n", trim: true) |> Stream.transform(1, &parse_cycles/2) |> Stream.drop(19) |> Stream.take_every(40) |> Stream.take(6) |> Enum.with_index(fn v, i -> (20 + 40 * i) * v end) |> Enum.sum() end def part2(input) do input |> String.split("\n", trim: true) |> Stream.transform(1, &parse_cycles/2) |> Stream.zip(Stream.cycle(0..39)) |> Stream.map(fn {signal, draw} when abs(draw - signal) <= 1 -> ?# _ -> ?. end) |> Stream.chunk_every(40) |> Enum.intersperse(?\n) |> to_string() end defp parse_cycles("noop", v), do: {[v], v} defp parse_cycles(<<"addx ", n::binary>>, v), do: {[v, v], v + String.to_integer(n)} end
-
Comment on Day 9: Rope Bridge in ~comp.advent_of_code
jzimbel (edited )Link ParentShortened with excessive weird one-line functions!! Also, I realized that Enum.scan/2 was perfect for propagating movements down the length of the rope. Both parts but shorter defmodule...Shortened with excessive weird one-line functions!! Also, I realized that
Enum.scan/2
was perfect for propagating movements down the length of the rope.Both parts but shorter
defmodule AdventOfCode.Solution.Year2022.Day09 do defstruct [:knots, visited: MapSet.new([{0, 0}])] def part1(input), do: solve(input, 2) def part2(input), do: solve(input, 10) defp solve(input, knot_count) do initial_state = %__MODULE__{knots: List.duplicate({0, 0}, knot_count)} final_state = input |> String.split("\n", trim: true) |> Enum.reduce(initial_state, &process_command/2) MapSet.size(final_state.visited) end for {dir, delta} <- [{?U, {0, -1}}, {?R, {1, 0}}, {?D, {0, 1}}, {?L, {-1, 0}}] do defp process_command(<<unquote(dir), " ", steps::binary>>, state) do for _ <- 1..String.to_integer(steps), reduce: state, do: (acc -> move(acc, unquote(delta))) end end defp move(%__MODULE__{knots: [h | t], visited: visited}, delta) do knots = Enum.scan([add(h, delta) | t], &propagate/2) visited = MapSet.put(visited, List.last(knots)) %__MODULE__{knots: knots, visited: visited} end defp propagate(t, h), do: if(adjacent?(h, t), do: t, else: move_toward(t, h)) defp adjacent?({x, y}, {x2, y2}), do: abs(x2 - x) <= 1 and abs(y2 - y) <= 1 defp move_toward({tx, ty}, {hx, hy}), do: add({tx, ty}, {get_move(hx - tx), get_move(hy - ty)}) defp get_move(0), do: 0 defp get_move(diff), do: div(diff, abs(diff)) defp add({x, y}, {x2, y2}), do: {x + x2, y + y2} end
-
Comment on Day 9: Rope Bridge in ~comp.advent_of_code
jzimbel Elixir I tried to "speedrun" this one but tripped myself up for a while on part 1 by referencing the previous value of the head when determining whether the tail was adjacent to it! Overall my...Elixir
I tried to "speedrun" this one but tripped myself up for a while on part 1 by referencing the previous value of the head when determining whether the tail was adjacent to it!
Overall my code is pretty messy, but I'm happy with how easy it was to adapt it to part 2. I'll probably come back later and clean it up.
Both parts
defmodule AdventOfCode.Solution.Year2022.Day09 do defstruct [:knots, visited: MapSet.new([{0, 0}])] @deltas %{ ?U => {0, -1}, ?R => {1, 0}, ?D => {0, 1}, ?L => {-1, 0} } def part1(input) do input |> String.split("\n", trim: true) |> Enum.reduce(%__MODULE__{knots: List.duplicate({0, 0}, 2)}, &process_command/2) |> then(fn state -> MapSet.size(state.visited) end) end def part2(input) do input |> String.split("\n", trim: true) |> Enum.reduce(%__MODULE__{knots: List.duplicate({0, 0}, 10)}, &process_command/2) |> then(fn state -> MapSet.size(state.visited) end) end defp process_command(<<dir, ?\s, n::binary>>, state) do delta = @deltas[dir] Enum.reduce(1..String.to_integer(n), state, fn _, state_acc -> move(state_acc, delta) end) end defp move(%{knots: [h | ts], visited: visited}, delta) do h = sum_coords(h, delta) ts = propagate(ts, h) visited = MapSet.put(visited, List.last(ts)) %{knots: [h | ts], visited: visited} end defp propagate([], _), do: [] defp propagate([t | ts], h) do t = if adjacent?(h, t), do: t, else: move_toward(t, h) [t | propagate(ts, t)] end defp sum_coords({x, y}, {x2, y2}), do: {x + x2, y + y2} defp adjacent?({x, y}, {x2, y2}) do abs(x2 - x) <= 1 and abs(y2 - y) <= 1 end defp move_toward({tx, ty}, {hx, hy}) do x_move = case hx - tx do 0 -> 0 n when n > 0 -> 1 n when n < 0 -> -1 end y_move = case hy - ty do 0 -> 0 n when n > 0 -> 1 n when n < 0 -> -1 end {tx + x_move, ty + y_move} end end
Part 1 runs in about 7 ms, part 2 in about 15 ms.
-
Comment on Day 8: Treetop Tree House in ~comp.advent_of_code
jzimbel (edited )LinkElixir Since these kinds of character-grid based puzzle inputs are so common, I created a CharGrid module in previous years that simplifies parsing and lookups. I only needed to add one new...Elixir
Since these kinds of character-grid based puzzle inputs are so common, I created a
CharGrid
module in previous years that simplifies parsing and lookups. I only needed to add one newlines_of_cells/3
function (plus a couple related convenience functions) for this puzzle—you give it a set of target coordinates, and it returns a list of lists of the cells radiating out from that location in each direction.After that, I just needed to write code that counts the number of cells that satisfy the conditions for each puzzle part.
CharGrid
moduleOnly including the relevant parts for this puzzle.
defmodule AdventOfCode.CharGrid do @moduledoc "Data structure representing a finite grid of characters by a map of {x, y} => char" alias __MODULE__, as: T @type t :: %T{ grid: grid, width: non_neg_integer, height: non_neg_integer } @typep grid :: %{coordinates => char} @type coordinates :: {non_neg_integer, non_neg_integer} @type cell :: {coordinates, char} @enforce_keys ~w[grid width height]a defstruct @enforce_keys # List of coords that produce the 8 coordinates surrounding a given coord when added to it @all_adjacent_deltas for x <- -1..1, y <- -1..1, not (x == 0 and y == 0), do: {x, y} # List of coords that produce the 4 coordinates horizontally/vertically adjacent to a given coord when added to it @cardinal_adjacent_deltas for x <- -1..1, y <- -1..1, abs(x) + abs(y) == 1, do: {x, y} # List of coords that produce the 4 coordinates diagonally adjacent to a given coord when added to it @intercardinal_adjacent_deltas for x <- -1..1, y <- -1..1, abs(x) + abs(y) == 2, do: {x, y} @adjacency_deltas_by_type %{ all: @all_adjacent_deltas, cardinal: @cardinal_adjacent_deltas, intercardinal: @intercardinal_adjacent_deltas } @spec from_input(String.t()) :: t() def from_input(input) do charlists = input |> String.split() |> Enum.map(&String.to_charlist/1) height = length(charlists) width = length(hd(charlists)) grid = for {line, y} <- Enum.with_index(charlists), {char, x} <- Enum.with_index(line), into: %{} do {{x, y}, char} end %T{ grid: grid, width: width, height: height } end @doc "Gets the value at the given coordinates." @spec at(t(), coordinates) :: char | nil def at(%T{} = t, coords) do t.grid[coords] end @doc """ Returns a list of lists of cells in lines from the one at `coords`. Each list starts with the cell nearest `coords`, and radiates outward. The type of adjacency is determined by the third argument: - `:all` (default behavior): ``` O.O.O .OOO. OO*OO .OOO. O.O.O ``` - `:cardinal`: ``` ..O.. ..O.. OO*OO ..O.. ..O.. ``` - `:intercardinal`: ``` O...O .O.O. ..*.. .O.O. O...O ``` """ @spec lines_of_cells(t(), coordinates, adjacency_type) :: list(list(cell)) def lines_of_cells(%T{} = t, coords, adjacency_type \\ :all) do @adjacency_deltas_by_type[adjacency_type] |> Enum.map(&get_line_of_cells(t, &1, sum_coordinates(coords, &1))) end @doc """ Convenience function that behaves the same as `lines_of_cells/3`, but returns only the char value of each cell. """ @spec lines_of_values(t(), coordinates, adjacency_type) :: list(list(char)) def lines_of_values(%T{} = t, coords, adjacency_type \\ :all) do t |> lines_of_cells(coords, adjacency_type) |> Enum.map(fn line -> Enum.map(line, &elem(&1, 1)) end) end @doc """ Convenience function that behaves the same as `lines_of_cells/3`, but returns only the coordinates of each cell. """ @spec lines_of_coords(t(), coordinates, adjacency_type) :: list(list(coordinates)) def lines_of_coords(%T{} = t, coords, adjacency_type \\ :all) do t |> lines_of_cells(coords, adjacency_type) |> Enum.map(fn line -> Enum.map(line, &elem(&1, 0)) end) end defp get_line_of_cells(t, step, coords) do case at(t, coords) do nil -> [] val -> [{coords, val} | get_line_of_cells(t, step, sum_coordinates(coords, step))] end end defp sum_coordinates({x1, y1}, {x2, y2}), do: {x1 + x2, y1 + y2} end
Solution
Edit: Made the solution cleaner (but slightly slower) by removing code that ignores the grid perimeter.
defmodule AdventOfCode.Solution.Year2022.Day08 do alias AdventOfCode.CharGrid def part1(input) do input |> parse_sight_lines_by_tree() |> Enum.count(&visible?/1) end def part2(input) do input |> parse_sight_lines_by_tree() |> Enum.map(&scenic_score/1) |> Enum.max() end defp parse_sight_lines_by_tree(input) do grid = CharGrid.from_input(input) grid |> CharGrid.to_list() |> Enum.map(fn {coords, tree} -> {tree, CharGrid.lines_of_values(grid, coords, :cardinal)} end) end defp visible?({tree, sight_lines}) do Enum.any?(sight_lines, fn other_trees -> Enum.all?(other_trees, &(&1 < tree)) end) end defp scenic_score({tree, sight_lines}) do sight_lines |> Enum.map(&count_visible(&1, tree)) |> Enum.product() end # A little tricky because we want to include the first tree that blocks our view. # All of the relevant `Enum` functions omit the first element that fails the condition, # so I needed to define my own recursive function. defp count_visible([], _), do: 0 defp count_visible([tree | _trees], from_tree) when tree >= from_tree, do: 1 defp count_visible([_tree | trees], from_tree), do: 1 + count_visible(trees, from_tree) end
-
Comment on Day 7: No Space Left On Device in ~comp.advent_of_code
jzimbel Elixir A challenging puzzle for a functional language that only provides immutable data structures! I used a fun data structure (design pattern? 🤷) called a zipper, which provides an efficient and...Elixir
A challenging puzzle for a functional language that only provides immutable data structures!
I used a fun data structure (design pattern? 🤷) called a zipper, which provides an efficient and purely functional way to navigate and manipulate a deeply nested data structure.
The basic idea of it is that as you descend into the sub-nodes, parent nodes that have been traversed get removed from the structure and added to a stack of "context" structs. This way, all edits happen to the "root" node, and the "root" changes as you move around. When it's time to go back up a level, the parent node gets rebuilt from the stored context, and the node we were previously editing gets added as a child of the reconstructed parent. This can be repeated all the way back to the root node, at which point you've put the whole structure back together.
Directory struct
A simple struct to store a directory node. The struct originally had more fields (I also had a separate struct for the file entries) since I didn't know what part 2 might ask for, but I've pared everything down to just what was ultimately needed to solve both parts.
defmodule Dir do @type t :: %__MODULE__{ size: non_neg_integer(), dirs: %{String.t() => t()} } defstruct size: 0, dirs: %{} @spec sizes(t()) :: list(non_neg_integer()) def sizes(%__MODULE__{} = d) do [d.size] ++ Enum.flat_map(Map.values(d.dirs), &sizes/1) end end
Zipper implementation
The typespecs kind of clutter things up, but they might be helpful to explain what's going on?
defmodule Zipper.Context do @type t :: %__MODULE__{ name: String.t(), siblings_size: non_neg_integer(), dir_siblings: %{String.t() => Dir.t()} } defstruct [:name, :siblings_size, :dir_siblings] end defmodule Zipper do alias Zipper.Context @type t :: %__MODULE__{ focus: Dir.t(), context: list(Context.t()) } defstruct focus: %Dir{}, context: [] @spec to_dir(t()) :: Dir.t() def to_dir(%__MODULE__{} = z) do Enum.reduce(z.context, z.focus, &rebuild_parent/2) end @spec add_dir(t(), String.t()) :: t() def add_dir(%__MODULE__{} = z, name) do update_in(z.focus.dirs, &Map.put_new(&1, name, %Dir{})) end @spec add_size(t(), integer) :: t() def add_size(%__MODULE__{} = z, size) do update_in(z.focus.size, &(&1 + size)) end @spec down(t(), String.t()) :: t() def down(%__MODULE__{} = z, name) do %__MODULE__{ focus: Map.fetch!(z.focus.dirs, name), context: [make_context(z.focus, name) | z.context] } end @spec up(t()) :: t() def up(%__MODULE__{context: [context | rest]} = z) do %__MODULE__{context: rest, focus: rebuild_parent(context, z.focus)} end defp make_context(dir, cd_name) do %Context{ name: cd_name, siblings_size: dir.size - dir.dirs[cd_name].size, dir_siblings: Map.delete(dir.dirs, cd_name) } end defp rebuild_parent(context, from_dir) do %Dir{ size: context.siblings_size + from_dir.size, dirs: Map.put_new(context.dir_siblings, context.name, from_dir) } end end
Solution
defmodule AdventOfCode.Solution.Year2022.Day07 do def part1(input) do input |> parse_fs() |> Dir.sizes() |> Enum.reject(&(&1 > 100_000)) |> Enum.sum() end @capacity 70_000_000 @min_needed 30_000_000 def part2(input) do fs = parse_fs(input) unused_space = @capacity - fs.size min_to_delete = @min_needed - unused_space fs |> Dir.sizes() |> Enum.filter(&(&1 >= min_to_delete)) |> Enum.min() end defp parse_fs(input) do input |> String.split("\n", trim: true) # Skip "$ cd /" |> Enum.drop(1) |> Enum.reduce(%Zipper{}, &process_line/2) |> Zipper.to_dir() end defp process_line("$ ls", z), do: z defp process_line("$ cd ..", z), do: Zipper.up(z) defp process_line("$ cd " <> dir_name, z), do: Zipper.down(z, dir_name) defp process_line("dir " <> dir_name, z) do Zipper.add_dir(z, dir_name) end defp process_line(file_line, z) do {size, _} = Integer.parse(file_line) Zipper.add_size(z, size) end end
I kept track of each directory's size along the way, so combined with the zipper approach I think my solution is pretty efficient. Each part runs in 575 - 615 μs (and they each start from the string input—would be faster if I could reuse the parsed tree but my solution-runner utilities don't allow that).
-
Comment on What have you been eating, drinking, and cooking? in ~food
jzimbel It's getting colder and I've been sick the past week and a half, so my partner and I have been cooking a lot of comfort food. "Spicy pork noodle soup" - something like pho but with ingredients...It's getting colder and I've been sick the past week and a half, so my partner and I have been cooking a lot of comfort food.
- "Spicy pork noodle soup" - something like pho but with ingredients that are more readily available in basic western grocery stores. Delicious, filling, comes together shockingly fast. The onion garnish comes out best if you have a mandoline and slice it extra thin.
- Khichdi - A kind of lentil and rice stew. I tweaked this recipe substantially to use more shelf-stable ingredients. Super tasty, something like chili. The yogurt is essential to make the flavors pop. Would recommend skipping the coriander seeds or grinding them up before adding, as whole seeds add an unpleasant texture.
- Pasta aglio e olio - quick and tasty.
- Oatmeal with a pat of butter, maple syrup, and cinnamon.
-
Comment on Day 6: Tuning Trouble in ~comp.advent_of_code
jzimbel (edited )LinkElixir 👶 Both parts Wasn't sure what to name the function tbh. It's not really giving the index. defmodule AdventOfCode.Solution.Year2022.Day06 do def part1(input), do: index_of_marker(input, 4)...Elixir
👶
Both parts
Wasn't sure what to name the function tbh. It's not really giving the index.
defmodule AdventOfCode.Solution.Year2022.Day06 do def part1(input), do: index_of_marker(input, 4) def part2(input), do: index_of_marker(input, 14) defp index_of_marker(input, size) do input |> String.to_charlist() |> Stream.chunk_every(size, 1, :discard) |> Enum.find_index(&(MapSet.size(MapSet.new(&1)) == size)) |> Kernel.+(size) end end
About
|>
I abuse it in my solutions (because it's fun), but the
|>
("pipe") operator is basically just syntactic sugar.Functions in Elixir, for the most part, are not tightly coupled with data types. As a counterexample, arrays in JS have array functions as "methods"—
myArray.map(...)
. The method gets the "subject" array as an implicit firstthis
argument. There are no methods in Elixir. Data and logic are more strictly separated. Instead, we have functions where you explicitly pass the "subject" as the first argument:Enum.map(my_list, ...)
.|>
exists mostly to make "chaining" possible. It takes the result of the previous expression, and passes it as the first argument of the next function call. The code runs the same, but it reads more like a pipeline of sequential operations than one giant messy one-liner.One thing I really like about this approach is that, in referencing the module for each function call—e.g.
String.*
,Enum.*
,MapSet.*
—you can sort of annotate what type you're working with at each step. In a longer pipeline, it's often very easy to see the type transformations that are happening; something that you don't get with chained method calls in other languages.numbers_str = "1 2 25 93 8" sum = Enum.sum(Enum.map(String.split(numbers_str, " "), &String.to_integer/1)) # compiles to the exact same byte code as sum = numbers_str # We're using a function from the String module, so the subject is currently a string |> String.split(" ") # We're using a function from the Enum module, so the subject is currently an enumerable (a list, specifically) |> Enum.map(&String.to_integer/1) # Still an enumerable |> Enum.sum()
-
Comment on Day 5: Supply Stacks in ~comp.advent_of_code
jzimbel (edited )Link ParentIs mypy smart enough to take assert statements into account? You could stick an assert len(breakdown) == 3 in there and see how mypy responds. Edit: Oops, I misread your code. breakdown isn't the...Is mypy smart enough to take
assert
statements into account? You could stick anassert len(breakdown) == 3
in there and see how mypy responds.Edit: Oops, I misread your code.
breakdown
isn't the 3-tuple. But maybeassert
could help nonetheless. -
Comment on Day 5: Supply Stacks in ~comp.advent_of_code
jzimbel (edited )LinkElixir Most of the work for this one was parsing the input, IMO! Both parts defmodule AdventOfCode.Solution.Year2022.Day05 do def part1(input), do: solve(input, &Enum.reverse/1) def part2(input),...Elixir
Most of the work for this one was parsing the input, IMO!
Both parts
defmodule AdventOfCode.Solution.Year2022.Day05 do def part1(input), do: solve(input, &Enum.reverse/1) def part2(input), do: solve(input, &Function.identity/1) def solve(input, crane_fn) do {stacks, instrs} = parse(input) instrs |> Enum.reduce(stacks, &move(&1, &2, crane_fn)) |> Enum.sort_by(fn {stack_label, _stack} -> stack_label end) |> Enum.map(fn {_stack_label, [crate | _]} -> crate end) |> to_string() end defp move({source, dest, count}, stacks, crane_fn) do {to_move, to_remain} = Enum.split(stacks[source], count) stacks |> Map.put(source, to_remain) |> Map.update!(dest, &Enum.concat(crane_fn.(to_move), &1)) end defp parse(input) do [stack_lines, instr_lines] = String.split(input, "\n\n") {parse_stacks(stack_lines), parse_instrs(instr_lines)} end # Assumes crate labels are always 1 char # Assumes stack labels are always 1 digit and listed in order: "1 2 ... 9" defp parse_stacks(stack_lines) do stack_lines |> String.split("\n", trim: true) # Faster to build lists tail-first |> Enum.reverse() # Skip last line with stack labels |> Enum.drop(1) |> Enum.map(&parse_stacks_row/1) |> Enum.reduce(fn stacks_row, stacks -> Map.merge(stacks, stacks_row, fn _stack_label, stack, [crate] -> [crate | stack] end) end) end defp parse_stacks_row(line) do line |> String.to_charlist() |> Enum.drop(1) |> Enum.take_every(4) |> Enum.with_index(fn crate, i -> {i + 1, [crate]} end) |> Enum.reject(&match?({_stack_label, ' '}, &1)) |> Map.new() end defp parse_instrs(instr_lines) do instr_lines |> String.split("\n", trim: true) |> Enum.map(fn line -> ~r/^move (\d+) from (\d) to (\d)$/ |> Regex.run(line, capture: :all_but_first) |> Enum.map(&String.to_integer/1) |> then(fn [count, from_label, to_label] -> {from_label, to_label, count} end) end) end end
-
Comment on Day 4: Camp Cleanup in ~comp.advent_of_code
jzimbel Elixir Both parts defmodule AdventOfCode.Solution.Year2022.Day04 do def part1(input) do input |> String.split("\n", trim: true) |> Enum.map(&parse_ints/1) |> Enum.count(&superset_subset?/1) end...Elixir
Both parts
defmodule AdventOfCode.Solution.Year2022.Day04 do def part1(input) do input |> String.split("\n", trim: true) |> Enum.map(&parse_ints/1) |> Enum.count(&superset_subset?/1) end def part2(input) do input |> String.split("\n", trim: true) |> Enum.map(&parse_ints/1) |> Enum.count(&overlap?/1) end defp parse_ints(line) do ~r/^(\d+)-(\d+),(\d+)-(\d+)$/ |> Regex.run(line, capture: :all_but_first) |> Enum.map(&String.to_integer/1) end defp superset_subset?([a, b, x, y]) do (a <= x and b >= y) or (a >= x and b <= y) end defp overlap?([a, b, x, y]) do not Range.disjoint?(a..b, x..y) end end
-
Comment on Day 3: Rucksack Reorganization in ~comp.advent_of_code
jzimbel Elixir Both parts Pretty straightforward. I converted the appropriate strings or substrings to sets of characters, and then used set intersection to find the common character. defmodule...Elixir
Both parts
Pretty straightforward. I converted the appropriate strings or substrings to sets of characters, and then used set intersection to find the common character.
defmodule AdventOfCode.Solution.Year2022.Day03 do def part1(input) do input |> String.split("\n", trim: true) |> Enum.map(&find_duplicate/1) |> Enum.map(&priority/1) |> Enum.sum() end def part2(input) do input |> String.split("\n", trim: true) |> Enum.chunk_every(3) |> Enum.map(&find_badge/1) |> Enum.map(&priority/1) |> Enum.sum() end defp find_duplicate(sack) do compartment_size = div(byte_size(sack), 2) sack |> String.to_charlist() |> Enum.split(compartment_size) |> then(fn {l, r} -> MapSet.intersection(MapSet.new(l), MapSet.new(r)) end) |> Enum.at(0) end defp find_badge(group) do group |> Enum.map(&(&1 |> String.to_charlist() |> MapSet.new())) |> Enum.reduce(&MapSet.intersection/2) |> Enum.at(0) end for {item, pri} <- Enum.with_index(Enum.concat(?a..?z, ?A..?Z), 1) do defp priority(unquote(item)), do: unquote(pri) end end
They also released .mov