jzimbel's recent activity

  1. Comment on What are some of your "life hacks" you use regularly? in ~talk

    jzimbel
    Link
    I forget where I learned it, and whether or not it’s common knowledge, but I get a surprising amount of use out of a simple procedure for solving ratios/proportions/unit conversions. It’s useful...

    I forget where I learned it, and whether or not it’s common knowledge, but I get a surprising amount of use out of a simple procedure for solving ratios/proportions/unit conversions. It’s useful for modifying ingredient amounts in recipes on the fly, and basically anything else that involves scaling.

    Say you have a ratio like this:

    X   6
    — = —
    4   8
    

    You can quickly solve for X by multiplying the 2 numbers it’s adjacent to: 4 * 6, then dividing the product by the number opposite X: (4 * 6) / 8 = 3.

    X can be in any of the 4 positions and the method will still work—the above equation would work out the same as any of these reflections of it (this is just a few examples out of many more):

    8   6
    — = —
    4   X
    
    6   8
    — = —
    X   4
    
    6   X
    — = —
    8   4
    
    8 votes
  2. Comment on Day 12: Christmas Tree Farm in ~comp.advent_of_code

    jzimbel
    Link Parent
    Wait, where's the hover text? I can't find any, even after poking around in the page markup.

    Wait, where's the hover text? I can't find any, even after poking around in the page markup.

  3. Comment on Day 10: Factory in ~comp.advent_of_code

    jzimbel
    Link
    Elixir I'll need to revisit part 2 when I have a good chunk of time to refresh my linear algebra knowledge. I haven't really touched that stuff in 10+ years, and never in a programming context. I...

    Elixir

    I'll need to revisit part 2 when I have a good chunk of time to refresh my linear algebra knowledge. I haven't really touched that stuff in 10+ years, and never in a programming context.

    I might dip my toes into Elixir's scientific computing ecosystem for this. It includes Elixir-based versions of many counterparts from Python-land, like numpy, polars/pandas, Jupyter notebooks, and pytorch.

    Anyway,

    Part 1

    Like some others, I noticed that the buttons could be represented as bitmasks that are XOR'ed with 0 to try and produce a target integer: the light diagram, also represented as a binary number.

    I use an additional "meta-bitmask" for selecting which of the button bitmasks to apply, since we need to check the entire power set of the buttons to find the minimal number of presses. The best way I could think of to generate a power set of S is to iterate through [0, 2(cardinality of S) - 1], using the iterated value as the meta-bitmask.

    Some of my code (particularly the comprehensions) looks a bit cursed.
    Elixir is a very... punctuation-rich language.

    defmodule AdventOfCode.Solution.Year2025.Day10 do
      import Bitwise, only: [&&&: 2, |||: 2, >>>: 2, <<<: 2]
    
      use AdventOfCode.Solution.SharedParse
    
      # Bitwise XOR should have an operator too! So let's make one >:]
      defdelegate a <~> b, to: Bitwise, as: :bxor
    
      @impl true
      def parse(input) do
        for line <- String.split(input, "\n", trim: true) do
          [diagram | others] = line |> String.split() |> Enum.map(&String.slice(&1, 1..-2//1))
          {buttons, [jolts]} = Enum.split(others, -1)
          {goal, bit_width} = parse_goal(diagram)
          {goal, Enum.map(buttons, &parse_mask(&1, bit_width)), jolts}
        end
      end
    
      def part1(machines) do
        machines
        |> Task.async_stream(&min_button_presses_p1/1, ordered: false)
        |> Enum.sum_by(fn {:ok, min_presses} -> min_presses end)
      end
    
      defp min_button_presses_p1({goal, masks, _}) do
        bit_flag_to_mask = Enum.with_index(masks, fn mask, i -> {2 ** i, mask} end)
    
        1..(2 ** length(masks) - 1)//1
        |> Stream.filter(&(apply_masks(bit_flag_to_mask, &1) == goal))
        |> Enum.reduce_while(:infinity, fn
          _, 1 -> {:halt, 1}
          meta_mask, min_acc -> {:cont, min(min_acc, sum_bits(meta_mask))}
        end)
      end
    
      defp apply_masks(bit_flag_to_mask, meta_mask) do
        for({b, mask} <- bit_flag_to_mask, (b &&& meta_mask) != 0, reduce: 0, do: (n -> n <~> mask))
      end
    
      defp sum_bits(n, acc \\ 0)
      defp sum_bits(0, acc), do: acc
      defp sum_bits(n, acc), do: sum_bits(n >>> 1, acc + (n &&& 1))
    
      defp parse_goal(s) do
        {for(<<c <- s>>, reduce: 0, do: (n -> (n <<< 1) + if(c == ?#, do: 1, else: 0))), byte_size(s)}
      end
    
      defp parse_mask(btn, goal_bit_width) do
        for <<c <- btn>>, c in ?0..?9, reduce: 0, do: (n -> n ||| 2 ** (goal_bit_width - 1 - c + ?0))
      end
    end
    
    Benchmarks
    Name             ips        average  deviation         median         99th %
    Parse         765.93        1.31 ms     ±1.94%        1.31 ms        1.36 ms
    Part 1        194.67        5.14 ms    ±29.27%        4.38 ms        8.17 ms
    
  4. Comment on Day 7: Laboratories in ~comp.advent_of_code

    jzimbel
    Link
    Elixir Pretty straightforward, just count the unique splitters encountered for part 1 and count the total number of unique paths for part 2. This was another good candidate for my new StringGrid...

    Elixir

    Pretty straightforward, just count the unique splitters encountered for part 1 and count the total number of unique paths for part 2. This was another good candidate for my new StringGrid struct.

    Both parts

    I went back and smushed everything into fewer lines for no obvious reason. (Certainly not readability...)

    defmodule AdventOfCode.Solution.Year2025.Day07 do
      alias AdventOfCode.StringGrid, as: SG
    
      use AdventOfCode.Solution.SharedParse
    
      @impl true
      def parse(input), do: {SG.new(input), input |> :binary.match("S") |> elem(0)}
    
      def part1({grid, start_x}),
        do: grid |> reduce_grid({[start_x], 0}, &sum_splits/3) |> then(fn {_xs, sum} -> sum end)
    
      def part2({grid, start_x}),
        do: grid |> reduce_grid(%{start_x => 1}, &count_paths/3) |> Map.values() |> Enum.sum()
    
      defp reduce_grid(grid, init_acc, reducer),
        do: for(y <- 0..(grid.height - 3)//1, reduce: init_acc, do: (acc -> reducer.(y, acc, grid)))
    
      defp sum_splits(y, {xs, sum}, grid) do
        xs
        |> Stream.map(&{&1, grid[{&1, y + 1}]})
        |> Enum.flat_map_reduce(0, fn
          {x, ?.}, n -> {[x], n}
          {x, ?^}, n -> {[x - 1, x + 1], n + 1}
        end)
        |> then(fn {xs, num_splits} -> {Enum.uniq(xs), sum + num_splits} end)
      end
    
      defp count_paths(y, counts_by_x, grid) do
        counts_by_x
        |> Stream.map(fn {x, n} -> {x, n, grid[{x, y + 1}]} end)
        |> Enum.reduce(%{}, fn
          {x, n, ?.}, acc -> Map.update(acc, x, n, &(&1 + n))
          {x, n, ?^}, acc -> acc |> Map.update(x - 1, n, &(&1 + n)) |> Map.update(x + 1, n, &(&1 + n))
        end)
      end
    end
    
    Benchmarks
    Name             ips        average  deviation         median         99th %
    Parse      4866.31 K        0.21 μs  ±9313.72%       0.167 μs        0.25 μs
    Part 1        1.65 K      605.77 μs     ±4.12%      600.58 μs      715.32 μs
    Part 2        1.49 K      673.21 μs     ±5.37%      660.25 μs      797.15 μs
    
    1 vote
  5. Comment on Day 6: Trash Compactor in ~comp.advent_of_code

    jzimbel
    Link
    Elixir I finished this one a few days ago but I'm playing catch-up on posting. I decided to create a new module / struct that allows characters in a grid-shaped puzzle input to be looked up by {x,...

    Elixir

    I finished this one a few days ago but I'm playing catch-up on posting.

    I decided to create a new module / struct that allows characters in a grid-shaped puzzle input to be looked up by {x, y} coordinates without ever splitting the string apart. This is something I've wanted to try for a while as it seemed like it could greatly improve performance of solutions where it could be used in place of my usual Grid module.

    And, I was right! Also it was really not that difficult to implement, just a little bit of math.

    New StringGrid struct

    (Trimmed down a bit from the full module for brevity)

    defmodule AdventOfCode.StringGrid do
      @moduledoc """
      Structure to facilitate accessing chars from a grid-shaped string by `{x,y}` index.
    
      `{0,0}` is the first character in the string, i.e., the top left corner of the grid.
      """
      alias __MODULE__, as: T
      use TypedStruct
    
      defguardp is_in_bounds(sg, x, y)
                when x in 0..(sg.width - 1)//1 and y in 0..(sg.height - 1)//1
    
      typedstruct enforce: true do
        field :s, String.t()
        field :width, non_neg_integer
        field :height, non_neg_integer
      end
    
      def new(s) do
        {width, _} = :binary.match(s, "\n")
        height = div(byte_size(s), width + 1)
        %T{s: s, width: width, height: height}
      end
    
      def at(%T{} = sg, {x, y}) when is_in_bounds(sg, x, y) do
        :binary.at(sg.s, x + y * (sg.width + 1))
      end
    
      def at(%T{}, xy), do: raise("not in bounds: #{inspect(xy)}")
    end
    
    Both parts

    The basic approach is to convert each expression into a list of [operator, term1, term2, ..., term_n]. and call eval/1 on it to get the result.

    For part 1, I take the most straightforward route and split the string into a list of lists of operators/terms, then transpose the whole thing (while also reversing the results) so that each sub-list now contains the operator and terms of a single expression, starting with the operator and moving upward. Then just pass each of these sub-lists through eval/1 and sum all the results up.

    For part 2, I figured out that it's possible to solve it by reducing over the string in a single pass. Moving from right to left, reading characters from one column at a time top-to-bottom, assemble the terms in an accumulator struct (the State struct defined at the start of the Part 2 section) and then apply the operator to them once it's reached, adding the result to a :sum field and resetting the rest of the map.

    defmodule AdventOfCode.Solution.Year2025.Day06 do
      alias AdventOfCode.StringGrid, as: SG
    
      defp eval([?+ | ns]), do: Enum.sum(ns)
      defp eval([?* | ns]), do: Enum.product(ns)
    
      ##########
      # Part 1 #
      ##########
    
      def part1(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(fn line ->
          line
          |> String.split()
          |> Enum.map(fn
            "+" -> ?+
            "*" -> ?*
            n -> String.to_integer(n)
          end)
        end)
        |> reverse_transpose()
        |> Enum.sum_by(&eval/1)
      end
    
      defp reverse_transpose(lists, acc \\ [])
    
      defp reverse_transpose([[] | _], acc), do: acc
    
      defp reverse_transpose(lists, acc) do
        {tails, col_rev} = Enum.map_reduce(lists, [], fn [h | t], acc -> {t, [h | acc]} end)
        reverse_transpose(tails, [col_rev | acc])
      end
    
      ##########
      # Part 2 #
      ##########
    
      use TypedStruct
    
      typedstruct module: State do
        field :sum, non_neg_integer, default: 0
        field :terms, [pos_integer], default: []
        field :term, pos_integer | nil, default: nil
      end
    
      def part2(input) do
        grid = SG.new(input)
    
        %State{sum: sum} =
          for x <- (grid.width - 1)..0//-1,
              y <- 0..(grid.height - 1)//1,
              reduce: %State{} do
            state -> process_char(grid[{x, y}], state)
          end
    
        sum
      end
    
      defp process_char(?\s, %{term: nil} = state), do: state
    
      defp process_char(?\s, %{term: n} = state) do
        %{state | terms: [n | state.terms], term: nil}
      end
    
      defp process_char(op, %{term: nil} = state) when op in ~c"+*" do
        %State{sum: state.sum + eval([op | state.terms])}
      end
    
      defp process_char(op, state) when op in ~c"+*" do
        process_char(op, %{state | terms: [state.term | state.terms], term: nil})
      end
    
      defp process_char(n, %{term: nil} = state), do: %{state | term: n - ?0}
    
      defp process_char(n, state), do: %{state | term: state.term * 10 + n - ?0}
    end
    
    Benchmarks
    Name             ips        average  deviation         median         99th %
    Part 1        3.46 K      289.30 μs    ±11.90%      284.46 μs      385.37 μs
    Part 2        1.38 K      722.32 μs     ±5.46%      723.17 μs      801.38 μs
    
    1 vote
  6. Comment on Day 9: Movie Theater in ~comp.advent_of_code

    jzimbel
    Link Parent
    Yeah, I was worried about this and a few other possible pitfalls while working on my solution. I spent a substantial amount of time writing checks to confirm all of the following: perimeter does...

    Yeah, I was worried about this and a few other possible pitfalls while working on my solution.

    I spent a substantial amount of time writing checks to confirm all of the following:

    • perimeter does not cross itself
    • every red tile is a corner of the shape--no 3 consecutive red tiles are collinear
    • input walks the perimeter in clockwise direction (this didn't end up mattering all that much)
    • perimeter never touches itself--every perimeter tile is adjacent to exactly 2 other perimeter tiles
    1 vote
  7. Comment on Day 5: Cafeteria in ~comp.advent_of_code

    jzimbel
    Link Parent
    Oh wow, I did not realize in (which is just a macro that compiles to an Enum.member?/2 call, I believe) had that much overhead for ranges! If I change the anonymous fn passed to Enum.any?/2 in my...

    Oh wow, I did not realize in (which is just a macro that compiles to an Enum.member?/2 call, I believe) had that much overhead for ranges!

    If I change the anonymous fn passed to Enum.any?/2 in my part 1 solution to this:

    &(id >= &1.first and id <= &1.last)
    

    it has the exact same performance as yours.

    Seems like the generalized logic for ranges with steps other than 1 slows things down a lot. (There's also some overhead from dispatching via the Enumerable protocol.)

    I wonder why they don't just add a separate clause with the faster logic for the most commonly used step size of 1.

    Re: limited type options, OOP concept relations

    Yeah, the main composite types available in elixir are much simpler than what you can cook up in most OOP languages. There are also structs, but they're basically just maps with well-defined keys.

    The limited inventory of core types is partly due to how deeply pattern matching is baked into the language and even the BEAM VM. Simple types allow for powerful, fast pattern matching.

    Also related: a big part of Elixir and the larger OTP ecosystem, which you unfortunately won't get much practice with on AoC puzzles, is stateful message-passing processes like Agents and GenServers. These are conceptually analogous to objects: they bundle some well-defined data structure with functions that act on it and a public/private interface. The big difference is that they behave more like mini-servers internal to your application, running receive-act-respond loops, with strong concurrency guarantees.

    1 vote
  8. Comment on Day 5: Cafeteria in ~comp.advent_of_code

    jzimbel
    Link
    Elixir Both parts I figured it would help to merge overlapping ranges for part 1, and it turned out to be necessary for part 2. I went a little overboard trying to squeeze stuff onto single lines,...

    Elixir

    Both parts I figured it would help to merge overlapping ranges for part 1, and it turned out to be necessary for part 2.

    I went a little overboard trying to squeeze stuff onto single lines, my code was more legible earlier. Ah well.

    defmodule AdventOfCode.Solution.Year2025.Day05 do
      use AdventOfCode.Solution.SharedParse
    
      @impl true
      def parse(input) do
        [ranges, ids] = String.split(input, "\n\n")
        {parse_and_consolidate_ranges(ranges), parse_ids(ids)}
      end
    
      def part1({ranges, ids}), do: Enum.count(ids, fn id -> Enum.any?(ranges, &(id in &1)) end)
      def part2({ranges, _ids}), do: Enum.sum_by(ranges, &Range.size/1)
    
      defp consolidate(ranges) do
        [hd_range | ranges] = Enum.sort_by(ranges, & &1.first)
    
        Enum.chunk_while(
          ranges,
          hd_range,
          fn _..b2//1 = r2, a1..b1//1 = r1 ->
            if Range.disjoint?(r1, r2), do: {:cont, r1, r2}, else: {:cont, a1..max(b1, b2)//1}
          end,
          fn final_range -> {:cont, final_range, nil} end
        )
      end
    
      defp parse_and_consolidate_ranges(str) do
        str
        |> String.split()
        |> Enum.map(fn line ->
          [first, last] = String.split(line, "-")
          String.to_integer(first)..String.to_integer(last)//1
        end)
        |> consolidate()
      end
    
      defp parse_ids(str) do
        str
        |> String.split()
        |> Enum.map(&String.to_integer/1)
      end
    end
    
    Benchmarks
    Name             ips        average  deviation         median         99th %
    Part 2     1401.14 K        0.71 μs   ±888.65%        0.71 μs        0.83 μs
    Parse         8.53 K      117.22 μs     ±4.82%      118.33 μs      131.04 μs
    Part 1        1.05 K      956.85 μs     ±1.20%      956.63 μs      983.61 μs
    
    3 votes
  9. Comment on Day 4: Printing Department in ~comp.advent_of_code

    jzimbel
    (edited )
    Link
    Elixir I've built up a pretty robust Grid module over the years—maybe concerningly robust given that it's used solely for a bunch of toy code puzzles—so today's solution was a quick one. No...

    Elixir

    I've built up a pretty robust Grid module over the years—maybe concerningly robust given that it's used solely for a bunch of toy code puzzles—so today's solution was a quick one.

    No special optimizations, I just took the obvious approach.

    Both parts
    defmodule AdventOfCode.Solution.Year2025.Day04 do
      alias AdventOfCode.Grid, as: G
    
      use AdventOfCode.Solution.SharedParse
    
      @impl true
      def parse(input), do: G.from_input(input)
    
      def part1(grid), do: map_size(forklift_cells(grid))
    
      def part2(grid) do
        grid
        |> Stream.unfold(&remove_forkliftable/1)
        |> Enum.sum()
      end
    
      defp forklift_cells(grid) do
        for {coords, ?@} <- grid, forkliftable?(coords, grid), into: %{}, do: {coords, ?.}
      end
    
      defp forkliftable?(coords, grid) do
        grid
        |> G.adjacent_values(coords)
        |> Enum.count(&(&1 == ?@))
        |> Kernel.<(4)
      end
    
      defp remove_forkliftable(grid) do
        removals = forklift_cells(grid)
    
        case map_size(removals) do
          # End the stream.
          0 -> nil
          n -> {n, G.replace(grid, removals)}
        end
      end
    end
    
    Benchmarks
    Name             ips        average  deviation         median         99th %
    Parse         526.68        1.90 ms     ±1.87%        1.90 ms        1.97 ms
    Part 1        159.39        6.27 ms     ±3.94%        6.27 ms        6.95 ms
    Part 2          7.69      130.08 ms     ±0.99%      129.91 ms      134.38 ms
    
    Bonus: Animated passes

    I exported an image of the grid after each forklift pass and assembled a looping animation.

    3 votes
  10. Comment on Day 3: Lobby in ~comp.advent_of_code

    jzimbel
    (edited )
    Link
    Elixir I'm including my original code for part 1 even though my part 2 solution can be used for both. Because I'm proud of my inscrutable recursive function 🥰 My general max_joltage/2 function...

    Elixir

    I'm including my original code for part 1 even though my part 2 solution can be used for both. Because I'm proud of my inscrutable recursive function 🥰

    My general max_joltage/2 function implements what @Hvv describes better than I could in their "But can we do it faster?" box.
    edit: Actually I think I went only halfway with the optimizations. I didn't do as much preprocessing as I could have! (And didn't keep the input rows as strings, and didn't do one of the skip-ahead optimizations)

    Parse input

    Note: ?<unicode codepoint> is a funky bit of built-in syntax that gives the integer value of a unicode codepoint. For basic ascii characters, it's equivalent to C's '<character>' syntax. For example, ?a == 97.

    When the puzzle input string is a bunch of digit characters that need to be parsed into a list of integers, digit_char - ?0 is a little shortcut for parsing each one.

    use AdventOfCode.Solution.SharedParse
    
    @impl true
    @spec parse(String.t()) :: [[0..9]]
    def parse(input) do
      input
      |> String.split()
      |> Enum.map(&for(d <- String.to_charlist(&1), do: d - ?0))
    end
    
    Original part 1 solution
    def part1(battery_banks) do
      Enum.sum_by(battery_banks, &max_joltage_p1/1)
    end
    
    defp max_joltage_p1(batteries, acc \\ {0, 0})
    
    defp max_joltage_p1([], {tens, ones}), do: Integer.undigits([tens, ones])
    defp max_joltage_p1([b], {tens, ones}) when b > ones, do: max_joltage_p1([], {tens, b})
    defp max_joltage_p1([_b], acc), do: max_joltage_p1([], acc)
    defp max_joltage_p1([b | bs], {tens, _ones}) when b > tens, do: max_joltage_p1(bs, {b, 0})
    defp max_joltage_p1([b | bs], {tens, ones}) when b > ones, do: max_joltage_p1(bs, {tens, b})
    defp max_joltage_p1([_b | bs], acc), do: max_joltage_p1(bs, acc)
    
    General solution for both parts
    def part1(battery_banks), do: Enum.sum_by(battery_banks, &max_joltage(&1, 2))
    def part2(battery_banks), do: Enum.sum_by(battery_banks, &max_joltage(&1, 12))
    
    defp max_joltage(batteries, num_to_activate) do
      bank_size = length(batteries)
      init_active_group = List.duplicate(0, num_to_activate)
    
      batteries
      # Annotate batteries with the earliest index they can occupy in the activated group
      |> Enum.with_index(fn b, i -> {b, max(num_to_activate + i - bank_size, 0)} end)
      |> Enum.reduce(init_active_group, &update_active_group/2)
      |> Integer.undigits()
    end
    
    defp update_active_group({b, min_i}, active_group) do
      {ineligible, eligible} = Enum.split(active_group, min_i)
      update_active_group(b, eligible, Enum.reverse(ineligible))
    end
    
    defp update_active_group(_b, [], acc), do: Enum.reverse(acc)
    
    defp update_active_group(b, [active | actives], acc) when b > active,
      do: update_active_group(b, [], List.duplicate(0, length(actives)) ++ [b | acc])
    
    defp update_active_group(b, [active | actives], acc),
      do: update_active_group(b, actives, [active | acc])
    
    Benchmarks

    (Using the general solution--which for part 1 is a few hundred μs slower than the bespoke solution)

    Name             ips        average  deviation         median         99th %
    Parse         4.18 K      239.18 μs     ±3.65%      235.08 μs      261.13 μs
    Part 1        2.68 K      373.56 μs    ±14.17%      370.92 μs      408.97 μs
    Part 2        0.84 K     1188.87 μs     ±1.98%     1190.63 μs     1230.33 μs
    
    4 votes
  11. Comment on Day 2: Gift Shop in ~comp.advent_of_code

    jzimbel
    (edited )
    Link
    Elixir I did my best to optimize my part 1 solution by filtering out large chunks of the ID ranges that would not work. (hint: the ID needs to be able to split into two parts with equal numbers of...

    Elixir

    I did my best to optimize my part 1 solution by filtering out large chunks of the ID ranges that would not work. (hint: the ID needs to be able to split into two parts with equal numbers of digits.)

    But none of that optimization really applies to part 2, so I did basically a brute-force approach for it. It took over a second to complete before I tweaked it to process all rows concurrently using Task.async_stream/3. Running concurrently, it takes about half a second.

    Parsing the input
    use AdventOfCode.Solution.SharedParse
    
    @impl true
    def parse(input) do
      input
      |> String.trim()
      |> String.split(",")
      |> Enum.map(&parse_range/1)
    end
    
    defp parse_range(str) do
      ~r/(\d+)-(\d+)/
      |> Regex.run(str, capture: :all_but_first)
      |> Enum.map(&String.to_integer/1)
      |> then(fn [first, last] -> first..last//1 end)
    end
    
    Part 1
    import Integer, only: [is_odd: 1]
    
    def part1(ranges) do
      ranges
      |> Task.async_stream(&(&1 |> invalid_ids_p1() |> Enum.sum()), ordered: false)
      |> Enum.sum_by(fn {:ok, sum} -> sum end)
    end
    
    defp invalid_ids_p1(range) do
      range = clamp_range(range)
      exponent = exponent_of_10(range.first)
      splitter = Integer.pow(10, div(exponent, 2) + 1)
      Stream.filter(range, &(div(&1, splitter) == rem(&1, splitter)))
    end
    
    # Shrinks the range so that it contains only numbers with even numbers of digits.
    defp clamp_range(first..last//1) do
      clamp_up(first)..clamp_down(last)//1
    end
    
    # Note: In my input, none of the ranges are so large that they include numbers
    # within multiple odd exponents of 10, e.g. 10-1000.
    #
    # So clamping the single range is enough, ranges never need to be split into
    # multiple sub-ranges to remove even exponents of 10 in the middle.
    
    defp clamp_up(n) do
      exponent = exponent_of_10(n)
      if is_odd(exponent), do: n, else: Integer.pow(10, exponent + 1)
    end
    
    defp clamp_down(n) do
      exponent = exponent_of_10(n)
      if is_odd(exponent), do: n, else: Integer.pow(10, exponent) - 1
    end
    
    defp exponent_of_10(n), do: floor(:math.log10(n))
    
    Part 2
    def part2(ranges) do
      ranges
      |> Task.async_stream(&(&1 |> invalid_ids_p2() |> Enum.sum()), ordered: false)
      |> Enum.sum_by(fn {:ok, sum} -> sum end)
    end
    
    defp invalid_ids_p2(range) do
      Stream.filter(range, &invalid_p2?/1)
    end
    
    defp invalid_p2?(n) do
      digits = Integer.digits(n)
      len = length(digits)
    
      # Generate chunk sizes to try splitting the digits up into
      1..div(len, 2)//1
      # Chunk size must divide the digits cleanly with no smaller leftover chunk at the end
      |> Stream.filter(&(div(len, &1) == len / &1))
      |> Enum.any?(fn chunk_size ->
        digits
        |> Enum.chunk_every(chunk_size)
        |> Enum.uniq()
        |> length()
        |> Kernel.==(1)
      end)
    end
    
    Benchmarks

    Without concurrency:

    Name             ips        average  deviation         median         99th %
    Parse       32028.96      0.00003 s    ±23.95%      0.00003 s      0.00007 s
    Part 1        118.94      0.00841 s     ±1.76%      0.00835 s      0.00875 s
    Part 2          0.52         1.91 s     ±0.99%         1.92 s         1.92 s
    

    With concurrency:

    Name             ips        average  deviation         median         99th %
    Parse       32601.48      0.0307 ms    ±21.58%      0.0284 ms      0.0701 ms
    Part 1        331.88        3.01 ms     ±6.84%        3.02 ms        3.46 ms
    Part 2          2.02      496.24 ms     ±1.39%      495.07 ms      510.70 ms
    
    2 votes
  12. Comment on Day 1: Secret Entrance in ~comp.advent_of_code

    jzimbel
    Link Parent
    It's a fun language! It's the primary language I use at work as well, so if you have any questions I can probably answer them. (Do note that I don't really prioritize clarity or readability for a...

    It's a fun language! It's the primary language I use at work as well, so if you have any questions I can probably answer them.

    (Do note that I don't really prioritize clarity or readability for a lot of my AoC puzzle solutions, though. I sometimes go out of my way to solve the puzzles in weird unconventional ways.)

    2 votes
  13. Comment on Day 1: Secret Entrance in ~comp.advent_of_code

    jzimbel
    (edited )
    Link
    Elixir If anyone is interested, I have a template repository that helps you set up an elixir project for Advent of Code puzzles with some nice conveniences. That’s what I’ll be using in all of my...

    Elixir

    If anyone is interested, I have a template repository that helps you set up an elixir project for Advent of Code puzzles with some nice conveniences. That’s what I’ll be using in all of my solutions.

    I am so glad he’s shortened the event to 12 days, I always spend way too much time on these puzzles and end up neglecting more important things during an already busy holiday month…

    Both parts

    I wanted to find a cleaner (i.e. more math-based, less branching-logic-based) approach for counting the number of times a given rotation visited position 0, but this was the best I could come up with.

    Summary of my approach: Build a list where each element is a map with :position and :zero_visits keys. Each map gives the position of the dial after one rotation specified by the input, as well as the number of times it moved to 0 on its way to that ending position. Use these computed states of the dial after each rotation to get the answers for both part 1 and part 2.

    defmodule AdventOfCode.Solution.Year2025.Day01 do
      use AdventOfCode.Solution.SharedParse
    
      @start_position 50
    
      @impl true
      def parse(input) do
        input
        |> String.split()
        |> Enum.map(fn
          "L" <> digits -> -String.to_integer(digits)
          "R" <> digits -> String.to_integer(digits)
        end)
        |> Stream.scan(%{position: @start_position, zero_visits: 0}, &move/2)
        |> Enum.to_list()
      end
    
      def part1(movements), do: Enum.count(movements, &(&1.position == 0))
      def part2(movements), do: Enum.sum_by(movements, & &1.zero_visits)
    
      defp move(rotation, %{position: position}) do
        %{
          position: Integer.mod(position + rotation, 100),
          zero_visits: count_zero_visits(position, rotation)
        }
      end
    
      # Note: Input never contains "L0" or "R0"--magnitude of a rotation is always nonzero.
      defp count_zero_visits(position, rotation) when rotation > 0, do: zv(rotation, 100 - position)
      defp count_zero_visits(position, rotation) when rotation < 0, do: zv(abs(rotation), position)
    
      defp zv(rot_mag, _zero_dist = 0), do: div(rot_mag, 100)
      defp zv(rot_mag, zero_dist) when rot_mag >= zero_dist, do: 1 + zv(rot_mag - zero_dist, 0)
      defp zv(_rot_mag, _zero_dist), do: 0
    end
    
    Benchmarks

    Since I put basically all the work in the shared parse function, "part 1" and "part 2" are simply the logic that sums up the results with one final pass through the list.

    Name             ips        average  deviation         median         99th %
    Part 2       93.12 K       10.74 μs    ±37.65%       10.63 μs       12.08 μs
    Part 1       29.50 K       33.90 μs     ±4.94%       33.71 μs       36.13 μs
    Parse         2.52 K      396.37 μs    ±30.97%      367.54 μs      546.53 μs
    
    3 votes
  14. Comment on Two signs that Democrats flipped Donald Trump supporters on Tuesday (gifted link) in ~society

  15. Comment on What are some interesting landmarks in your neck of the woods? in ~talk

    jzimbel
    Link
    Taco bench. Hexagon house. Tree wizard. (Sadly, someone set him on fire a few years ago, but He Is Immortal.)

    Taco bench.
    Hexagon house.
    Tree wizard. (Sadly, someone set him on fire a few years ago, but He Is Immortal.)

    1 vote
  16. Comment on iOS 26 is here in ~tech

    jzimbel
    Link Parent
    Oh wow, that Finder window screenshot is egregious… I can almost sense the resentment coming through from whichever poor dev/design team was tasked with updating the Finder UI

    Oh wow, that Finder window screenshot is egregious… I can almost sense the resentment coming through from whichever poor dev/design team was tasked with updating the Finder UI

    8 votes
  17. Comment on Norway's capital is known for its green policies and widespread adoption of electric vehicles. Why does the city still struggle with air pollution? in ~enviro

    jzimbel
    Link
    Now I’m imagining pea-sized dust motes drifting through the air over Oslo (The article is off by a factor of 1000—PM10 is in micrometers, not millimeters)

    In February, levels of PM10 pollution — from tiny but harmful airborne particles of less than 10 millimeters diameter

    Now I’m imagining pea-sized dust motes drifting through the air over Oslo

    (The article is off by a factor of 1000—PM10 is in micrometers, not millimeters)

    15 votes
  18. Comment on James Bond shocker! Amazon MGM Studios takes creative control of spy franchise as producers Michael G. Wilson and Barbara Broccoli step back. in ~movies

    jzimbel
    Link Parent
    Along these same lines, I saw this very funny bluesky post yesterday.

    Along these same lines, I saw this very funny bluesky post yesterday.

    3 votes
  19. Comment on Are modern iPhones unusable without a case? in ~comp

    jzimbel
    Link
    As someone who does not use a case, there’s a reason why basically the only thing I check when deciding whether to buy a newer iPhone is the weight and dimensions. I won’t even consider it if it’s...

    As someone who does not use a case, there’s a reason why basically the only thing I check when deciding whether to buy a newer iPhone is the weight and dimensions. I won’t even consider it if it’s any heavier or wider/taller than my current phone.

    (I missed the boat on the 13 mini, sadly)

    1 vote
  20. Comment on What are your favourite things to mix with natural yogurt? in ~food

    jzimbel
    Link
    For Greek yogurt, this dip you can make in about a half hour is absolutely delicious. Have made many times, goes great with pita (toasted or not), carrots, fennel, etc. Around my area, Fage Total...

    For Greek yogurt, this dip you can make in about a half hour is absolutely delicious. Have made many times, goes great with pita (toasted or not), carrots, fennel, etc.

    Around my area, Fage Total 5% milkfat is the best plain Greek yogurt for this recipe