jzimbel's recent activity
-
Comment on What are some of your "life hacks" you use regularly? in ~talk
-
Comment on Day 12: Christmas Tree Farm in ~comp.advent_of_code
jzimbel Link ParentWait, 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.
-
Comment on Day 10: Factory in ~comp.advent_of_code
jzimbel LinkElixir 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, andpytorch.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 endBenchmarks
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 -
Comment on Day 7: Laboratories in ~comp.advent_of_code
jzimbel LinkElixir 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
StringGridstruct.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 endBenchmarks
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 -
Comment on Day 6: Trash Compactor in ~comp.advent_of_code
jzimbel LinkElixir 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 usualGridmodule.And, I was right! Also it was really not that difficult to implement, just a little bit of math.
New
StringGridstruct(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)}") endBoth parts
The basic approach is to convert each expression into a list of
[operator, term1, term2, ..., term_n]. and calleval/1on 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/1and 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
Statestruct 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:sumfield 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} endBenchmarks
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 -
Comment on Day 9: Movie Theater in ~comp.advent_of_code
jzimbel Link ParentYeah, 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
-
Comment on Day 5: Cafeteria in ~comp.advent_of_code
jzimbel Link ParentOh 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 anEnum.member?/2call, I believe) had that much overhead for ranges!If I change the anonymous fn passed to
Enum.any?/2in 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
Enumerableprotocol.)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 andGenServers. 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. -
Comment on Day 5: Cafeteria in ~comp.advent_of_code
jzimbel LinkElixir 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 endBenchmarks
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 -
Comment on Day 4: Printing Department in ~comp.advent_of_code
jzimbel (edited )LinkElixir 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
Gridmodule 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 endBenchmarks
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 msBonus: Animated passes
I exported an image of the grid after each forklift pass and assembled a looping animation.
-
Comment on Day 3: Lobby in ~comp.advent_of_code
jzimbel (edited )LinkElixir 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/2function 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 - ?0is 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)) endOriginal 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 -
Comment on Day 2: Gift Shop in ~comp.advent_of_code
jzimbel (edited )LinkElixir 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) endPart 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) endBenchmarks
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 sWith 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 -
Comment on Day 1: Secret Entrance in ~comp.advent_of_code
jzimbel Link ParentIt'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.)
-
Comment on Day 1: Secret Entrance in ~comp.advent_of_code
jzimbel (edited )LinkElixir 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
:positionand:zero_visitskeys. 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 endBenchmarks
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 -
Comment on Two signs that Democrats flipped Donald Trump supporters on Tuesday (gifted link) in ~society
-
Comment on What are some interesting landmarks in your neck of the woods? in ~talk
jzimbel LinkTaco 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.) -
Comment on iOS 26 is here in ~tech
jzimbel Link ParentOh 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 UIOh 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
-
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 LinkNow 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)
-
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 ParentAlong these same lines, I saw this very funny bluesky post yesterday.Along these same lines, I saw this very funny bluesky post yesterday.
-
Comment on Are modern iPhones unusable without a case? in ~comp
jzimbel LinkAs 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)
-
Comment on What are your favourite things to mix with natural yogurt? in ~food
jzimbel LinkFor 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
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:
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):