jzimbel's recent activity

  1. Comment on Where are you on the spectrum of vacation planning? Detailed to the hour or floating like a leaf in the wind? in ~travel

    jzimbel
    Link Parent
    Funny you mention Alcatraz, because my partner and I took this approach for a visit to SF and had an absolutely wonderful time—we booked a table at a nice restaurant and made plans with relatives...

    Funny you mention Alcatraz, because my partner and I took this approach for a visit to SF and had an absolutely wonderful time—we booked a table at a nice restaurant and made plans with relatives in the area, and otherwise just chose a different city park to explore each day. It was super cheap and super fun to get around with the municipal electric bikeshare and public transit.

    1 vote
  2. Comment on Tildes Video Thread in ~misc

  3. Comment on Day 22: Sand Slabs in ~comp.advent_of_code

    jzimbel
    Link Parent
    Ah gotcha, I had missed that the first new brick (the lower of the two) caused one of the seven bricks from the original example to become load-bearing when it previously wasn't. So the new count...

    Ah gotcha, I had missed that the first new brick (the lower of the two) caused one of the seven bricks from the original example to become load-bearing when it previously wasn't. So the new count of 5 was composed of 4 from the existing set of 7, and 1 from the new pair.

    And yes, that fixed it!

    2 votes
  4. Comment on Day 22: Sand Slabs in ~comp.advent_of_code

    jzimbel
    Link Parent
    Sorry if this is resurfacing bad debugging memories, but how does that even work? Both of the two new bricks are load-bearing? If 1,2,11~1,4,11 starts above all the others, how can it be...

    Sorry if this is resurfacing bad debugging memories, but how does that even work? Both of the two new bricks are load-bearing? If 1,2,11~1,4,11 starts above all the others, how can it be load-bearing?

    (I think I'm missing the same edge case as you were, thanks for sharing the extra example!)

    1 vote
  5. Comment on Is there a programming language that brings you joy? in ~comp

    jzimbel
    Link
    It’s elixir for me—while the syntax can look a bit odd at first, you eventually realize it’s composed of a small number of basic elements with several layers of optional syntactic sugar on top. At...

    It’s elixir for me—while the syntax can look a bit odd at first, you eventually realize it’s composed of a small number of basic elements with several layers of optional syntactic sugar on top. At its core, syntactically, it’s similar to a lisp. The language’s creator has actually described it as a lisp-2 language once or twice. This allows for robust and relatively user-friendly macros.

    Separately from the syntax, the BEAM runtime’s focus on actor model/shared-nothing concurrency means you can (and are encouraged to) create applications that behave like a network of fault-tolerant mini-services rather than a single monolithic program. The way you structure your logic is just very different from other languages, and in a good way. You also don’t need to reach for additional technologies as often—for example, it’s trivial to set up a cache without memcached or the like, or cronjob-like tasks.

    Having used elixir as the backend language at my job for the past 3 years, I can truthfully say that tracking down and fixing bugs in our elixir code has never taken more than a couple hours. The immutable functional style within each elixir process, and the supervisor tree / actor model between processes, generally makes for a very resilient and observable system.

    6 votes
  6. Comment on ‘Winning requires hard work’: Wayfair CEO sends employees a gloomy pre-holiday email following layoff-filled year in ~finance

    jzimbel
    Link Parent
    Also worked at Wayfair, 2017-2020, as a SWE, and I also experienced a strong culture of “What are you still doing here at 5(:30), it’s not that serious, go home right now”. Upper management...

    Also worked at Wayfair, 2017-2020, as a SWE, and I also experienced a strong culture of “What are you still doing here at 5(:30), it’s not that serious, go home right now”. Upper management definitely leaned a bit sociopath, but in general most of the teams I worked on had decent managers who cared about their people’s wellbeing.

    Niraj sent out “story time” emails like this weekly, I believe, most with a theme of “work hard, work smart”.

    Not to defend the company—it was and is kind of a mess—but we all learned to basically tune out these emails from Niraj and co.
    Try asking around about the “WayThrifty” one. lol.

    10 votes
  7. Comment on Day 14: Reflector Dish in ~comp.advent_of_code

    jzimbel
    Link
    Elixir For some reason I have a lot of trouble wrapping my head around the math/logic to extrapolate a repeating sequence of values, not starting at index 0, to the nth element. This time, after...

    Elixir

    For some reason I have a lot of trouble wrapping my head around the math/logic to extrapolate a repeating sequence of values, not starting at index 0, to the nth element. This time, after working it out again, I actually bothered to write a little function to do it. No more tears on these problems in the future. 🥲

    New Sequence module

    The function itself is short, but I wanted to make sure I'd have an easy time remembering how to use it when the next iteration of this "find the bazillionth iteration of some sequence" problem comes up again.

    defmodule AdventOfCode.Sequence do
      @moduledoc """
      Functions for infinitely repeating sequences.
      """
    
      @doc """
      Given an indexed list describing the repeating segment of an infinitely repeating sequence,
      returns the value at `at_index` of the sequence.
    
      Indices in the list do not need to start at 0, but they do need to be contiguous,
      and the list must be ordered by index, ascending.
    
      `at_index` can be any integer.
      """
      @spec at(nonempty_list({a, index :: integer}), integer) :: a when a: term
      def at(repeat, i) do
        [{_value, start_index} | _] = repeat
        {value, _index} = Enum.at(repeat, Integer.mod(i - start_index, length(repeat)))
        value
      end
    end
    
    Part 1 and part 2

    I find it very intuitive to model this kind of "cycle of repeated operations" problem as a stream pipeline.

    defmodule AdventOfCode.Solution.Year2023.Day14 do
      alias AdventOfCode.Grid, as: G
      alias AdventOfCode.Sequence
    
      def part1(input) do
        input
        |> G.from_input()
        |> tilt(:n)
        |> rock_load()
      end
    
      def part2(input) do
        [:n, :w, :s, :e]
        |> Stream.cycle()
        |> Stream.scan(G.from_input(input), &tilt(&2, &1))
        |> Stream.drop(3)
        |> Stream.take_every(4)
        |> Stream.with_index(1)
        |> Enum.reduce_while(%{}, fn {grid, i}, indices ->
          case Map.fetch(indices, grid) do
            {:ok, repeat_start} -> {:halt, build_repeat_segment(indices, repeat_start, i - 1)}
            :error -> {:cont, Map.put(indices, grid, i)}
          end
        end)
        |> Sequence.at(1_000_000_000)
      end
    
      defp build_repeat_segment(indices, start_index, end_index) do
        repeat_indices = start_index..end_index//1
    
        indices
        |> Stream.filter(fn {_g, j} -> j in repeat_indices end)
        |> Stream.map(fn {g, j} -> {rock_load(g), j} end)
        |> Enum.sort_by(&elem(&1, 1))
      end
    
      defp tilt(grid, :n), do: do_tilt(grid, &G.cols/1, ?O, &G.from_cols/1)
      defp tilt(grid, :s), do: do_tilt(grid, &G.cols/1, ?., &G.from_cols/1)
      defp tilt(grid, :w), do: do_tilt(grid, &G.rows/1, ?O, &G.from_rows/1)
      defp tilt(grid, :e), do: do_tilt(grid, &G.rows/1, ?., &G.from_rows/1)
    
      defp do_tilt(grid, to_lanes, char_to_send_forward, from_lanes) do
        grid
        |> to_lanes.()
        |> Enum.map(fn lane ->
          lane
          |> Stream.map(fn {_coords, char} -> char end)
          |> Stream.chunk_by(&(&1 == ?#))
          |> Enum.flat_map(fn
            [?# | _] = l -> l
            l -> char_to_front(l, char_to_send_forward)
          end)
        end)
        |> from_lanes.()
      end
    
      defp char_to_front(l, char, acc \\ [])
      defp char_to_front([], _char, acc), do: acc
      defp char_to_front([char | rest], char, acc), do: [char | char_to_front(rest, char, acc)]
      defp char_to_front([other | rest], char, acc), do: char_to_front(rest, char, [other | acc])
    
      defp rock_load(grid) do
        grid
        |> G.rows()
        |> Stream.with_index()
        |> Stream.map(fn {row, i} -> (grid.height - i) * Enum.count(row, &match?({_, ?O}, &1)) end)
        |> Enum.sum()
      end
    end
    
    1 vote
  8. Comment on Day 15: Lens Library in ~comp.advent_of_code

    jzimbel
    Link Parent
    It seemed a little weird to me as well for day 15, especially with how much they spelled out the instructions compared to past days from this year. Kind of a basic data structures problem.

    It seemed a little weird to me as well for day 15, especially with how much they spelled out the instructions compared to past days from this year. Kind of a basic data structures problem.

    2 votes
  9. Comment on Day 15: Lens Library in ~comp.advent_of_code

    jzimbel
    Link
    Elixir Very easy! List.keystore/4 and List.keydelete/3 were perfect for the map-building operations. Parts 1 and 2 Squeezed down to 24 mostly-illegible lines, because I felt like it defmodule...

    Elixir

    Very easy! List.keystore/4 and List.keydelete/3 were perfect for the map-building operations.

    Parts 1 and 2

    Squeezed down to 24 mostly-illegible lines, because I felt like it

    defmodule AdventOfCode.Solution.Year2023.Day15 do
      def part1(input), do: input |> parse() |> Stream.map(&hash/1) |> Enum.sum()
      def part2(input), do: input |> parse() |> to_map() |> stream_focusing_powers() |> Enum.sum()
    
      defp to_map(ops) do
        ops
        |> Stream.map(&Regex.run(~r/([^=]+)(?:-|=(.+))/, &1, capture: :all_but_first))
        |> Enum.reduce(Map.new(0..255, &{&1, []}), fn
          [i, f], m -> Map.update!(m, hash(i), &List.keystore(&1, i, 0, {i, String.to_integer(f)}))
          [i], m -> Map.update!(m, hash(i), &List.keydelete(&1, i, 0))
        end)
      end
    
      defp stream_focusing_powers(map) do
        Stream.flat_map(map, fn {box, lenses} ->
          lenses
          |> Stream.with_index(1)
          |> Stream.map(fn {{_label, f}, slot} -> (1 + box) * slot * f end)
        end)
      end
    
      defp hash(str), do: for(<<char <- str>>, reduce: 0, do: (acc -> rem((acc + char) * 17, 256)))
      defp parse(input), do: input |> String.split(",", trim: true) |> Stream.map(&String.trim/1)
    end
    
    2 votes
  10. Comment on Day 16: The Floor Will Be Lava in ~comp.advent_of_code

    jzimbel
    Link
    Elixir My code is pretty incomprehensible because I was messing around with macros to define functions with "unusual" names 🌝 The macro tomfoolery essentially allows the solution to turn the...

    Elixir

    My code is pretty incomprehensible because I was messing around with macros to define functions with "unusual" names 🌝

    The macro tomfoolery essentially allows the solution to turn the grid's tiles into direct function calls, no map lookups or anonymous functions necessary. It didn't really improve performance, but I had fun seeing what kind of nonsense I could get into.

    Parts 1 and 2
    defmodule AdventOfCode.Solution.Year2023.Day16 do
      defmodule Silliness do
        defmacro __using__(_) do
          quote do
            import Kernel, only: :macros
            def unquote(:.)(h), do: [h]
            def unquote(:/)({x, y}), do: [{-y, -x}]
            def unquote(:"\\")({x, y}), do: [{y, x}]
            def unquote(:-)({0, _}), do: [{-1, 0}, {1, 0}]
            def unquote(:-)(h), do: [h]
            def unquote(:|)({_, 0}), do: [{0, -1}, {0, 1}]
            def unquote(:|)(h), do: [h]
          end
        end
      end
    
      defmodule TurnOps, do: use(Silliness)
    
      alias AdventOfCode.Grid, as: G
    
      defstruct [:beams, history: MapSet.new()]
    
      def part1(input) do
        input
        |> G.from_input(&String.to_existing_atom(<<&1>>))
        |> count_energized({{0, 0}, {1, 0}})
      end
    
      def part2(input) do
        grid = G.from_input(input, &String.to_existing_atom(<<&1>>))
    
        grid
        |> stream_init_beam_states()
        |> Task.async_stream(&count_energized(grid, &1), ordered: false)
        |> Stream.map(fn {:ok, count} -> count end)
        |> Enum.max()
      end
    
      defp count_energized(grid, init_beam_state) do
        %__MODULE__{beams: [init_beam_state]}
        |> Stream.iterate(&step_beams(&1, grid))
        |> Enum.find(&match?(%{beams: []}, &1))
        |> then(& &1.history)
        |> MapSet.new(fn {coords, _heading} -> coords end)
        |> MapSet.size()
      end
    
      defp step_beams(%{beams: beams, history: history}, grid) do
        %__MODULE__{
          beams: Enum.flat_map(beams, &if(&1 in history, do: [], else: step_beam(&1, grid))),
          history: MapSet.union(history, MapSet.new(beams))
        }
      end
    
      defp step_beam({coords, heading}, %{width: w, height: h} = grid) do
        apply(TurnOps, G.at(grid, coords), [heading])
        |> Stream.map(fn new_heading -> {sum_coords(coords, new_heading), new_heading} end)
        |> Enum.filter(fn {{x, y}, _} -> x in 0..(w - 1)//1 and y in 0..(h - 1)//1 end)
      end
    
      defp stream_init_beam_states(%{width: w, height: h}) do
        [
          Stream.flat_map(1..(h - 2)//1, fn y -> [{{0, y}, {1, 0}}, {{w - 1, y}, {-1, 0}}] end),
          Stream.flat_map(1..(w - 2)//1, fn x -> [{{x, 0}, {0, 1}}, {{x, h - 1}, {0, -1}}] end),
          Stream.flat_map([{0, 0}, {w - 1, 0}, {0, h - 1}, {w - 1, h - 1}], fn {x, y} ->
            x_headings = if x == 0, do: [0, 1], else: [0, -1]
            y_headings = if y == 0, do: [1, 0], else: [-1, 0]
    
            for heading <- Enum.zip(x_headings, y_headings), do: {{x, y}, heading}
          end)
        ]
        |> Stream.concat()
      end
    
      defp sum_coords({x1, y1}, {x2, y2}), do: {x1 + x2, y1 + y2}
    end
    
    1 vote
  11. Comment on Day 12: Hot Spring in ~comp.advent_of_code

    jzimbel
    (edited )
    Link
    Welp, my updated solution for part 2 is passing unit tests (including checking the count for each line individually) but giving too high an answer for the real input. It seems like I'm missing...

    Welp, my updated solution for part 2 is passing unit tests (including checking the count for each line individually) but giving too high an answer for the real input. It seems like I'm missing some edge case that exists only in the real input, but I couldn't begin to guess what that is.

    Could someone who has a working solution share their puzzle input, with each individual line tagged with its individual arrangement count? Only if you have time to help a stranger on the internet, ofc.
    Maybe via pastebin / GH gist, considering how big the input is...

    edit: I figured it out! I wasn't checking that the arrangement contains no '#'s after the last contiguous segment.

    Parts 1 and 2, now working

    After changing it to run on all lines concurrently, and tuning the cache table appropriately, it solves part 2 in about 1.8 sec on my machine. :)

    defmodule AdventOfCode.Solution.Year2023.Day12 do
      @table __MODULE__.MemoTable
    
      def part1(input) do
        input
        |> String.split("\n", trim: true)
        |> solve()
      end
    
      def part2(input) do
        input
        |> String.split("\n", trim: true)
        |> Stream.map(&quintuple/1)
        |> solve()
      end
    
      defp solve(lines) do
        :ets.new(@table, [
          :named_table,
          :public,
          read_concurrency: true,
          write_concurrency: true,
          decentralized_counters: true
        ])
    
        lines
        |> Stream.map(&parse_line/1)
        |> Task.async_stream(
          fn {pattern, contig_counts} ->
            memo_count_arrangements(pattern, contig_counts, 0)
          end,
          ordered: false
        )
        |> Stream.map(fn {:ok, count} -> count end)
        |> Enum.sum()
      after
        :ets.delete(@table)
      end
    
      defp quintuple(line) do
        [springs_str, sizes_str] = String.split(line)
    
        [{springs_str, "?"}, {sizes_str, ","}]
        |> Enum.map(fn {s, j} -> List.duplicate(s, 5) |> Enum.join(j) end)
        |> Enum.join(" ")
      end
    
      defp memo_count_arrangements(pattern, contig_counts, offset) do
        memo({pattern, contig_counts, offset}, fn ->
          count_arrangements(pattern, contig_counts, offset)
        end)
      end
    
      defp count_arrangements(pattern, [contig_count | _], offset)
           when byte_size(pattern) - offset < contig_count,
           do: 0
    
      defp count_arrangements(pattern, [], offset) do
        # If there are any '#'s in the remainder of the string, this arrangement is not valid.
        case :binary.match(pattern, "#", scope: {byte_size(pattern), offset - byte_size(pattern)}) do
          :nomatch -> 1
          _ -> 0
        end
      end
    
      defp count_arrangements(pattern, [contig_count | rest_contig], offset) do
        trailing_dot? = match?([_ | _], rest_contig)
        segment_size = contig_count + if(trailing_dot?, do: 1, else: 0)
    
        # The first '#' we find limits how far we can go. The last possible fit for this contig_count starts at that '#'.
        stop_after =
          case :binary.match(pattern, "#", scope: {byte_size(pattern), offset - byte_size(pattern)}) do
            {pos, _len} -> pos
            :nomatch -> :infinity
          end
    
        dot = if trailing_dot?, do: "[?.]", else: ""
    
        ~r"""
        [?\#]   # This is the character that we'll get the index of. The first character of this valid contiguous section.
        (?=     # Positive lookahead. Check that the correct characters follow the start of this contiguous section, without consuming them.
          [?\#]{#{contig_count - 1}}   # Remaining characters of the contiguous section.
          #{dot}                       # Optional trailing '.', if this is not the final contiguous section.
        )
        """x
        |> Regex.scan(pattern, offset: offset, return: :index)
        |> Stream.flat_map(fn [{pos, _len}] -> if pos <= stop_after, do: [pos], else: [] end)
        |> Stream.map(&memo_count_arrangements(pattern, rest_contig, &1 + segment_size))
        |> Enum.sum()
      end
    
      defp memo(key, fun) do
        case :ets.match(@table, {key, :"$1"}) do
          [[value]] -> value
          [] -> tap(fun.(), &:ets.insert(@table, {key, &1}))
        end
      end
    
      defp parse_line(line) do
        [springs_str, sizes_str] = String.split(line)
    
        {springs_str, Enum.map(String.split(sizes_str, ","), &String.to_integer/1)}
      end
    end
    
    2 votes
  12. Comment on Day 11: Cosmic Expansion in ~comp.advent_of_code

    jzimbel
    Link
    Elixir This one was pretty straightforward. One of the more esoteric Enum functions, flat_map_reduce/3, came in handy here. Parts 1 and 2 I had some extra time to tweak things and see how they...

    Elixir

    This one was pretty straightforward. One of the more esoteric Enum functions, flat_map_reduce/3, came in handy here.

    Parts 1 and 2

    I had some extra time to tweak things and see how they affected performance, so parts of the code are a bit more complicated or weird than they need to be, but hopefully it still mostly makes sense.

    To avoid having to deal with a sparsely-filled grid after expanding along one axis, I did the expansions separately (and concurrently), with each producing a map looking like original_galaxy_coordinates => new_x_or_y_coordinate. Then, I merged the two maps together to get the full new coordinates of each galaxy.

    defmodule AdventOfCode.Solution.Year2023.Day11 do
      alias AdventOfCode.CharGrid, as: G
    
      def part1(input), do: solve(input, 1)
      def part2(input), do: solve(input, 999_999)
    
      def solve(input, spacing) do
        input
        |> G.from_input()
        |> expanded_galaxy_coords(spacing)
        |> pairwise_distances_sum()
      end
    
      defp expanded_galaxy_coords(grid, spacing) do
        [new_xs, new_ys] =
          Task.await_many([
            Task.async(fn -> expanded_axis(grid, spacing, &G.cols/1, fn {x, _y} -> x end) end),
            Task.async(fn -> expanded_axis(grid, spacing, &G.rows/1, fn {_x, y} -> y end) end)
          ])
    
        Enum.map(new_xs, fn {coords, x} -> {x, new_ys[coords]} end)
      end
    
      defp expanded_axis(grid, spacing, lanes_fn, axis_fn) do
        grid
        |> lanes_fn.()
        |> Enum.flat_map_reduce(0, fn lane, expand_by ->
          if Enum.all?(lane, &match?({_coords, ?.}, &1)) do
            {[], expand_by + spacing}
          else
            {for({coords, ?#} <- lane, do: {coords, axis_fn.(coords) + expand_by}), expand_by}
          end
        end)
        |> then(fn {new_axis, _expand_by} -> Map.new(new_axis) end)
      end
    
      defp pairwise_distances_sum(galaxy_coords, sum \\ 0)
      defp pairwise_distances_sum([], sum), do: sum
    
      defp pairwise_distances_sum([{x1, y1} | rest], sum) do
        new_sums = for({x2, y2} <- rest, reduce: 0, do: (acc -> acc + abs(x2 - x1) + abs(y2 - y1)))
        pairwise_distances_sum(rest, sum + new_sums)
      end
    end
    
    1 vote
  13. Comment on Day 9: Mirage Maintenance in ~comp.advent_of_code

    jzimbel
    Link
    Elixir Not much to say about this one, pretty straightforward. My only mistake was accidentally rerunning my part 1 solution and submitting that as my part 2 answer after seeing my tests pass....

    Elixir

    Not much to say about this one, pretty straightforward. My only mistake was accidentally rerunning my part 1 solution and submitting that as my part 2 answer after seeing my tests pass.

    Parts 1 and 2

    I did a tiny bit of optimization by reversing the lists of numbers depending on which end I was interested in, so that I could do hd(list) (constant time) rather than List.last(list) (linear time). Probably could have avoided a bunch of near-duplicate code, or the non-tail-call recursion for part 2, but meh this works.

    defmodule AdventOfCode.Solution.Year2023.Day09 do
      def part1(input) do
        input
        |> parse_reverse()
        |> Enum.map(&extrapolate_forward/1)
        |> Enum.sum()
      end
    
      def part2(input) do
        input
        |> parse()
        |> Enum.map(&extrapolate_backward/1)
        |> Enum.sum()
      end
    
      defp extrapolate_forward(values, acc \\ 0)
    
      defp extrapolate_forward(values, acc) do
        if Enum.all?(values, &(&1 == 0)) do
          acc
        else
          values
          |> Enum.chunk_every(2, 1, :discard)
          # (List is reversed, so diff is left-right rather than right-left.)
          |> Enum.map(fn [b, a] -> b - a end)
          |> extrapolate_forward(acc + hd(values))
        end
      end
    
      defp extrapolate_backward(values)
    
      defp extrapolate_backward(values) do
        if Enum.all?(values, &(&1 == 0)) do
          0
        else
          sub_result =
            values
            |> Enum.chunk_every(2, 1, :discard)
            |> Enum.map(fn [a, b] -> b - a end)
            |> extrapolate_backward()
    
          hd(values) - sub_result
        end
      end
    
      defp parse_reverse(input) do
        for line <- String.split(input, "\n", trim: true) do
          line
          |> String.split()
          |> Stream.map(&String.to_integer/1)
          |> Enum.reverse()
        end
      end
    
      defp parse(input) do
        for line <- String.split(input, "\n", trim: true) do
          for num <- String.split(line), do: String.to_integer(num)
        end
      end
    end
    
    1 vote
  14. Comment on Day 8: Haunted Wasteland in ~comp.advent_of_code

    jzimbel
    Link
    Elixir As many others have pointed out, part 2 wasn't very fun because I initially considered that each "ghost" might not always start on step 0 of its respective loop, thus making it far more...

    Elixir

    As many others have pointed out, part 2 wasn't very fun because I initially considered that each "ghost" might not always start on step 0 of its respective loop, thus making it far more difficult to calculate where they match up. It wasn't so bad once I figured out that they all start on step 0 and don't have any "sub-loops" / pass through multiple __Z labels, but those properties weren't made clear by the puzzle description.

    New lil' Math module

    There have been a handful of puzzles that involve calculating GCD/LCM, and I always forget how, so I figured I'd create a reusable module for those and other slightly-more-than-basic math functions.

    defmodule AdventOfCode.Math do
      @moduledoc """
      Common math functions/algorithms.
      """
    
      @doc """
      Greatest common denominator.
      """
      @spec gcd(Enumerable.t(pos_integer)) :: pos_integer
      def gcd(ns), do: Enum.reduce(ns, &gcd/2)
    
      defp gcd(d, 0), do: d
      defp gcd(a, b), do: gcd(b, Integer.mod(a, b))
    
      @doc """
      Least common multiple.
      """
      @spec lcm(Enumerable.t(pos_integer)) :: pos_integer
      def lcm(ns), do: Enum.reduce(ns, &lcm/2)
    
      defp lcm(a, b), do: div(a * b, gcd(a, b))
    end
    
    Parts 1 and 2

    I've been stubbornly avoiding regexes for parsing inputs this year, because binary pattern matching is neat and weird. As long as the values you're trying to pull out of the string have a constant character length (variable length allowed if it's at the end of the string), binary pattern matching is sufficient.

    defmodule AdventOfCode.Solution.Year2023.Day08 do
      alias AdventOfCode.Math
    
      @type t :: %__MODULE__{
              labels: %{(index :: non_neg_integer) => String.t()},
              step: non_neg_integer,
              zs: MapSet.t(step :: non_neg_integer)
            }
      @enforce_keys [:labels]
      defstruct @enforce_keys ++ [step: 0, zs: MapSet.new()]
    
      def new(labels) do
        labels
        |> Enum.with_index(fn l, i -> {i, l} end)
        |> Map.new()
        |> then(&%__MODULE__{labels: &1})
      end
    
      def part1(input) do
        input
        |> parse()
        |> steps_to_z(["AAA"], &(&1 == "ZZZ"))
      end
    
      def part2(input) do
        input
        |> parse()
        |> steps_to_z(&match?(<<_, _, ?Z>>, &1))
      end
    
      defp steps_to_z({dirs, map}, done?) do
        steps_to_z({dirs, map}, for(<<_, _, ?A>> = label <- Map.keys(map), do: label), done?)
      end
    
      defp steps_to_z({dirs, map}, starting_labels, done?) do
        dirs
        |> Stream.cycle()
        |> Enum.reduce_while(new(starting_labels), fn dir, acc ->
          if map_size(acc.labels) == 0 do
            {:halt, acc.zs}
          else
            {:cont, update_acc(acc, map, dir, done?)}
          end
        end)
        |> Math.lcm()
      end
    
      defp update_acc(acc, map, dir, done?) do
        found_z = Enum.flat_map(acc.labels, fn {i, l} -> if done?.(l), do: [i], else: [] end)
    
        labels =
          acc.labels
          |> Map.drop(found_z)
          |> Map.new(fn {i, l} -> {i, map[l][dir]} end)
    
        zs = if match?([_ | _], found_z), do: MapSet.put(acc.zs, acc.step), else: acc.zs
    
        %{acc | labels: labels, zs: zs, step: acc.step + 1}
      end
    
      defp parse(input) do
        [dirs, map] = String.split(input, "\n\n")
    
        dirs = for <<dir::1-bytes <- dirs>>, do: dir |> String.downcase() |> String.to_existing_atom()
    
        map =
          for <<label::3-bytes, _::4*8, left::3-bytes, _::2*8, right::3-bytes, _::bytes>> <-
                String.split(map, "\n", trim: true),
              into: %{},
              do: {label, %{l: left, r: right}}
    
        {dirs, map}
      end
    end
    
    1 vote
  15. Comment on Day 6: Wait For It in ~comp.advent_of_code

    jzimbel
    Link
    Elixir Still playing catch-up, but this one was a nice break. I still did more work than necessary, though—I assumed part 2 would require optimization so I implemented a binary search with no...

    Elixir

    Still playing catch-up, but this one was a nice break. I still did more work than necessary, though—I assumed part 2 would require optimization so I implemented a binary search with no guardrails (since it's guaranteed to find a value for each race in the puzzle input) and used math to compute the total number of winners from the first winning button-hold value. Yay for me, I guess?

    Part 1 runs in ~12μs, part 2 in ~5.7μs. It takes longer to run multiple small searches than to run one big one! Thanks, O(log n)!.

    Parts 1 and 2
    defmodule AdventOfCode.Solution.Year2023.Day06 do
      import Integer, only: [is_even: 1]
    
      def part1(input) do
        input
        |> parse_p1()
        |> Enum.map(&count_wins/1)
        |> Enum.product()
      end
    
      def part2(input) do
        input
        |> parse_p2()
        |> count_wins()
      end
    
      defp parse_p1(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(fn line ->
          line
          |> String.split()
          |> Enum.drop(1)
          |> Enum.map(&String.to_integer/1)
        end)
        |> Enum.zip()
      end
    
      defp parse_p2(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(fn line ->
          line
          |> String.split(":")
          |> Enum.at(1)
          |> String.replace(" ", "")
          |> String.to_integer()
        end)
        |> List.to_tuple()
      end
    
      defp count_wins({t, d}) do
        half_t = div(t, 2)
        n = binary_search_first_win(1, half_t, t, d)
    
        if is_even(t), do: 2 * (half_t - n) + 1, else: 2 * (half_t - n + 1)
      end
    
      defp binary_search_first_win(n_min, n_max, t, d) do
        n = div(n_min + n_max, 2)
    
        with {:n_wins_race?, true} <- {:n_wins_race?, n * (t - n) > d},
             n_sub1 = n - 1,
             {:n_sub1_loses_race?, true} <- {:n_sub1_loses_race?, n_sub1 * (t - n_sub1) <= d} do
          n
        else
          {:n_wins_race?, false} -> binary_search_first_win(n + 1, n_max, t, d)
          {:n_sub1_loses_race?, false} -> binary_search_first_win(n_min, n - 1, t, d)
        end
      end
    end
    
    1 vote
  16. Comment on Day 7: Camel Cards in ~comp.advent_of_code

    jzimbel
    Link
    Elixir I split my part 1 and 2 solutions into separate modules, mostly just to namespace the functions so I didn't have to have a bunch of parse_p1 parse_p2 etc names. Common logic The sort key, a...

    Elixir

    I split my part 1 and 2 solutions into separate modules, mostly just to namespace the functions so I didn't have to have a bunch of parse_p1 parse_p2 etc names.

    Common logic

    The sort key, a tuple of the form {type_score :: integer, hand :: list(integer)} takes advantage of elixir/erlang's default term ordering. Unlike a lot of C-family languages, comparison operators like <, ==, etc do a deep comparison by default. Tuples are compared left to right, tie-breaker style. The second elements are checked only if the first elements are equal, and so on. The same goes for lists. So, using this value as a sort key lets me sort the list of plays exactly as the puzzle describes.

    defmodule AdventOfCode.Solution.Year2023.Day07 do
      def part1(input), do: solve(input, __MODULE__.Part1)
      def part2(input), do: solve(input, __MODULE__.Part2)
    
      def solve(input, mod) do
        input
        |> mod.parse()
        |> Enum.sort_by(fn {hand, _bid} -> {type_score(mod.card_groupings(hand)), hand} end)
        |> Enum.with_index(1)
        |> Enum.map(&winnings/1)
        |> Enum.sum()
      end
    
      defp type_score([5]), do: 6
      defp type_score([1, 4]), do: 5
      defp type_score([2, 3]), do: 4
      defp type_score([1, 1, 3]), do: 3
      defp type_score([1, 2, 2]), do: 2
      defp type_score([1, 1, 1, 2]), do: 1
      defp type_score([1, 1, 1, 1, 1]), do: 0
    
      defp winnings({{_hand, bid}, rank}), do: bid * rank
    end
    
    Logic specific to part 1
    defmodule AdventOfCode.Solution.Year2023.Day07.Part1 do
      def parse(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(&parse_play/1)
      end
    
      def card_groupings(hand) do
        hand |> Enum.frequencies() |> Map.values() |> Enum.sort()
      end
    
      defp parse_play(line) do
        [hand, bid] = String.split(line)
        hand = for <<char <- hand>>, do: parse_card_value(char)
        bid = String.to_integer(bid)
    
        {hand, bid}
      end
    
      for {char, value} <- Enum.zip(~c[TJQKA], 10..14) do
        defp parse_card_value(unquote(char)), do: unquote(value)
      end
    
      defp parse_card_value(digit_char), do: digit_char - ?0
    end
    
    Logic specific to part 2
    defmodule AdventOfCode.Solution.Year2023.Day07.Part2 do
      @j_value 1
    
      def parse(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(&parse_play/1)
      end
    
      def card_groupings(hand) do
        {jokers, hand} = Enum.split_with(hand, &(&1 == @j_value))
        j_count = length(jokers)
    
        hand
        |> AdventOfCode.Solution.Year2023.Day07.Part1.card_groupings()
        |> strengthen(j_count)
      end
    
      defp strengthen(groupings, 0), do: groupings
      defp strengthen([], 5), do: [5]
    
      defp strengthen(groupings, j_count) do
        {smaller, [biggest]} = Enum.split(groupings, -1)
        smaller ++ [biggest + j_count]
      end
    
      defp parse_play(line) do
        [hand, bid] = String.split(line)
        hand = for <<char <- hand>>, do: parse_card_value(char)
        bid = String.to_integer(bid)
    
        {hand, bid}
      end
    
      defp parse_card_value(?T), do: 10
      defp parse_card_value(?J), do: @j_value
    
      for {char, value} <- Enum.zip(~c[QKA], 12..14) do
        defp parse_card_value(unquote(char)), do: unquote(value)
      end
    
      defp parse_card_value(digit_char), do: digit_char - ?0
    end
    
    1 vote
  17. Comment on Day 5: If You Give A Seed A Fertilizer in ~comp.advent_of_code

    jzimbel
    Link
    Elixir I just got out of recursion hell implementing the optimized part 2 solution, and it's pretty wild to me that: the optimized approach solves part 2 in ~2.5ms when the naive one was still...

    Elixir

    I just got out of recursion hell implementing the optimized part 2 solution, and it's pretty wild to me that:

    • the optimized approach solves part 2 in ~2.5ms when the naive one was still chugging along after the better part of a day.
    • this is a day 4 puzzle! The optimization approach for this is similar to that of 2021's day 22 (!) puzzle. (Discussion and solution for that here, though there are probably easier-to-follow explanations out there by other folks.) Quaking in my boots at what's yet to come if we keep going at this rate.
    Messy part 1 and 2 solutions, featuring triple-nested Enum.reduce
    defmodule AdventOfCode.Solution.Year2023.Day05 do
      def part1(input), do: solve(input, &parse_seed_ranges_p1/1)
      def part2(input), do: solve(input, &parse_seed_ranges_p2/1)
    
      defp solve(input, seed_ranges_parser) do
        {seed_ranges, transforms} = parse(input, seed_ranges_parser)
    
        seed_ranges
        |> Stream.flat_map(&seed_range_to_location_ranges(&1, transforms))
        |> Stream.map(& &1.first)
        |> Enum.min()
      end
    
      defp parse(input, seed_ranges_parser) do
        [seeds_line | transform_lines] = String.split(input, "\n\n", trim: true)
    
        {seed_ranges_parser.(seeds_line), Enum.map(transform_lines, &parse_transform/1)}
      end
    
      defp parse_seed_ranges_p1("seeds: " <> seeds_str) do
        seeds_str
        |> String.split()
        |> Stream.map(&String.to_integer/1)
        |> Stream.map(&(&1..&1//1))
      end
    
      defp parse_seed_ranges_p2("seeds: " <> seeds_str) do
        seeds_str
        |> String.split()
        |> Stream.map(&String.to_integer/1)
        |> Stream.chunk_every(2)
        |> Stream.map(fn [start, len] -> start..(start + len - 1)//1 end)
      end
    
      defp parse_transform(transform_lines) do
        transform_lines
        |> String.split("\n", trim: true)
        |> Stream.drop(1)
        |> Stream.map(fn line ->
          line
          |> String.split()
          |> Enum.map(&String.to_integer/1)
        end)
        |> Enum.map(fn [dest_start, src_start, len] ->
          {src_start..(src_start + len - 1)//1, dest_start - src_start}
        end)
      end
    
      defp seed_range_to_location_ranges(seed_range, transforms) do
        Enum.reduce(transforms, [seed_range], &apply_transform/2)
      end
    
      defp apply_transform(transform_set, seed_ranges) do
        Enum.reduce_while(transform_set, %{unshifted: seed_ranges, shifted: []}, fn
          _, %{unshifted: []} = acc ->
            {:halt, acc}
    
          {window, shift_by}, acc ->
            updates =
              acc.unshifted
              |> Enum.reduce(%{unshifted: [], shifted: []}, fn seed_range, acc ->
                cond do
                  Range.disjoint?(window, seed_range) ->
                    Map.update!(acc, :unshifted, &[seed_range | &1])
    
                  window == seed_range ->
                    Map.update!(acc, :shifted, &[Range.shift(seed_range, shift_by) | &1])
    
                  true ->
                    {shifted_range, unshifted_ranges} =
                      intersect_and_shift(seed_range, window, shift_by)
    
                    acc
                    |> Map.update!(:shifted, &[shifted_range | &1])
                    |> Map.update!(:unshifted, &(unshifted_ranges ++ &1))
                end
              end)
    
            acc
            |> Map.put(:unshifted, updates.unshifted)
            |> Map.update!(:shifted, &(updates.shifted ++ &1))
            |> then(&{:cont, &1})
        end)
        |> then(&(&1.unshifted ++ &1.shifted))
      end
    
      defp intersect_and_shift(sl..sr//1, tl..tr//1, shift_by) do
        {
          Range.shift(max(sl, tl)..min(sr, tr)//1, shift_by),
          Enum.reject([sl..(tl - 1)//1, (tr + 1)..sr//1], &(Range.size(&1) == 0))
        }
      end
    end
    
    1 vote
  18. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    jzimbel
    Link Parent
    Ah ok, that's more what I'd expect. I was about to be pretty shocked by how much optimization the erlang VM does.

    Ah ok, that's more what I'd expect. I was about to be pretty shocked by how much optimization the erlang VM does.

    1 vote
  19. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    jzimbel
    Link Parent
    Out of curiosity, how are you timing your program? Is it 5ms to solve both parts? Asking because the benchmarks for my solution in elixir are consistently around 4.5 ms or less for each part—I...

    Out of curiosity, how are you timing your program? Is it 5ms to solve both parts? Asking because the benchmarks for my solution in elixir are consistently around 4.5 ms or less for each part—I really would not have expected it to run faster than a rust solution!

    Specifically, I'm timing only the evaluation of each solution function, which receives the puzzle input already loaded from file, as a string argument. Running on a 2019 MBP.

    $ mix do advent.solve -d4 -p1 -b + advent.solve -d4 -p2 -b
    # (Benchmark for part 1 solution fn)
    Benchmarking result ...
    
    Name             ips        average  deviation         median         99th %
    result        233.56        4.28 ms     ±6.42%        4.23 ms        5.42 ms
    
    # (Benchmark for part 2 solution fn)
    Benchmarking result ...
    
    Name             ips        average  deviation         median         99th %
    result        224.13        4.46 ms     ±9.05%        4.35 ms        6.04 ms
    
    1 vote
  20. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    jzimbel
    Link
    My partner and I bought and decorated a Christmas tree for the first time in our adult lives last night, so I was a little too tired to stay up past midnight and solve this one quickly. So many...

    My partner and I bought and decorated a Christmas tree for the first time in our adult lives last night, so I was a little too tired to stay up past midnight and solve this one quickly. So many pine needles, everywhere!!

    Elixir

    Parts 1 and 2

    I think the thing I had the most trouble with on this one was naming, honestly.
    Part 2 seemed like it would be a challenge, but turned out to be quite straightforward once I came up with sensible—if a bit verbose—labels for the two variables that describe each card: {self_copy_count, child_copy_count}.

    It was nice that you could get away with not parsing any numbers for this one. It works fine just by comparing the numeric strings.
    You don't even need to use the card IDs!

    defmodule AdventOfCode.Solution.Year2023.Day04 do
      def part1(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(&parse_card/1)
        |> Enum.map(&score_card/1)
        |> Enum.sum()
      end
    
      def part2(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(&parse_card/1)
        |> count_cards()
      end
    
      defp score_card({_, 0}), do: 0
      defp score_card({_, n}), do: Integer.pow(2, n - 1)
    
      defp count_cards(cards, acc \\ 0)
      defp count_cards([], acc), do: acc
    
      defp count_cards([{self_copy_count, 0} | children], acc) do
        count_cards(children, acc + self_copy_count)
      end
    
      defp count_cards([{self_copy_count, child_copy_count} | children], acc) do
        {children_to_copy, unchanged} = Enum.split(children, child_copy_count)
    
        new_children =
          for {sub_self_copy_count, sub_child_copy_count} <- children_to_copy do
            {sub_self_copy_count + self_copy_count, sub_child_copy_count}
          end
    
        count_cards(new_children ++ unchanged, acc + self_copy_count)
      end
    
      defp parse_card(line) do
        [_card_number, contents] = String.split(line, ": ")
    
        [winners_str, hand_str] = String.split(contents, " | ")
        winners = MapSet.new(String.split(winners_str))
        hand = MapSet.new(String.split(hand_str))
    
        {1, MapSet.size(MapSet.intersection(winners, hand))}
      end
    end
    
    2 votes