Advent of Code 2025 - Day 2

“When in doubt, use brute force.” – Ken Thompson

defmodule Day02 do
  def part1(input) do
    solve(input, &invalid_part1?/1)
  end

  defp invalid_part1?(n) when is_integer(n) and n > 0 do
    cond do
      n in 10..99 ->
        div(n, 10) === rem(n, 10)
      n in 1000..9999 ->
        div(n, 100) === rem(n, 100)
      n in 100_000..999_999 ->
        div(n, 1000) === rem(n, 1000)
      n in 10_000_000..99_999_999 ->
        div(n, 10_000) === rem(n, 10_000)
      n in 1_000_000_000..9_999_999_999 ->
        div(n, 100_000) === rem(n, 100_000)
      true -> false
    end
  end

  def part2(input) do
    solve(input, &invalid_part2?/1)
  end

  defp invalid_part2?(n) when is_integer(n) and n > 0 do
    powers = [10, 100, 1000, 10000, 100000, 1000000]
    Enum.any?(powers, fn power ->
      part = rem(n, power)
      if part < div(power, 10) do
        false
      else
        case count_parts(div(n, power), power, part, 1) do
          nil -> false
          num_parts -> num_parts >= 2
        end
      end
    end)
  end

  defp count_parts(0, _power, _part, num_parts), do: num_parts
  defp count_parts(n, power, part, num_parts) do
    case rem(n, power) do
      ^part ->
        count_parts(div(n, power), power, part, num_parts + 1)
      _ ->
        nil
    end
  end

  defp solve(input, invalid) do
    parse(input)
    |> Enum.flat_map(&expand_range(&1, invalid))
    |> Enum.sum
  end

  defp expand_range(r, invalid) do
    Enum.flat_map(r, fn n ->
      case invalid.(n) do
        true -> [n]
        false -> []
      end
    end)
  end

  defp parse(input) do
    input
    |> Enum.flat_map(fn line ->
      line
      |> String.split(",")
      |> Enum.map(fn range ->
        range
        |> String.split("-")
        |> then(fn [first, last] ->
          String.to_integer(first) .. String.to_integer(last)
        end)
      end)
    end)
  end
end

4 Likes

I just halved the string and checked for both halves being equal for part 1 and good old regex for part 2.

2 Likes

2025 Dec 02

Gift Shop

defmodule ProductId do
  def parse_range(s) do
    case Regex.run(~r"(\d+)-(\d+)", s) do
      [_match, ls, hs] ->
        String.to_integer(ls)..String.to_integer(hs)
    end
  end
  def parse_ranges(input) do 
    String.split(input, ",")
      |> Enum.map(&parse_range/1)
  end

  def invalid1?(id) do
    s = Integer.to_string(id)
    l = String.length(s)
    if Bitwise.band(l, 1) == 1 do
      false
    else
      {s1, s2} = String.split_at(s, div(l, 2))
      s1 == s2
    end
  end

  def invalids(seqs, invalid? \\ &invalid1?/1) do
    Task.async_stream(seqs, fn seq ->
      Enum.filter(seq, invalid?)
    end) |> 
      Enum.flat_map(fn {:ok, invs} -> invs end)
  end
end
test_ranges = """
11-22,95-115,998-1012,1188511880-1188511890,222220-222224,
1698522-1698528,446443-446449,38593856-38593862,565653-565659,
824824821-824824827,2121212118-2121212124
""" |> ProductId.parse_ranges()
ProductId.invalids(test_ranges)
  |> IO.inspect()
  |> Enum.sum()
input_ranges = File.read!(__DIR__ <> "/dec-02-input.txt")
  |> ProductId.parse_ranges()
ProductId.invalids(input_ranges)
  |> IO.inspect()
  |> Enum.sum()

Part Two

defmodule ProductId2 do
  def invalid?(id) do
    s = Integer.to_string(id)
    l = String.length(s)
    if l < 2 do
      false
    else
      Enum.map(1..div(l, 2), fn i ->
        if rem(l, i) != 0 do
          false
        else
          s0 = String.slice(s, 0..(i-1))
          s == String.duplicate(s0, div(l, i))
        end
      end)
        |> Enum.any?()
    end
  end
end
ProductId.invalids(test_ranges, &ProductId2.invalid?/1)
  |> IO.inspect()
  |> Enum.sum()
ProductId.invalids(input_ranges, &ProductId2.invalid?/1)
  |> IO.inspect()
  |> Enum.sum()
2 Likes

I’ve also gone for the brute-force option. Not very elegant and slow (3s or so), luckily the input didn’t make it impossible to just brute force it so I didn’t have to think hard about a clever solution (although I am intrigued and hope to see some clever ways here later today!).

aoc 2025, 2 do
  def p1(input) do
    input
    |> parse_input
    |> Enum.filter(&repeating_id?(&1))
    |> Enum.sum()
  end

  def p2(input) do
    input
    |> parse_input
    |> Enum.filter(&any_repeating_ids?(&1))
    |> Enum.sum()
  end

  defp repeating_id?(id) do
    digits = Integer.digits(id)
    {l, r} = Enum.split(digits, div(length(digits), 2))
    l == r
  end

  defp any_repeating_ids?(id) do
    digits = Integer.digits(id)
    half_len = div(length(digits), 2)

    for pair_len <- 1..half_len//1 do
      [first | _] = chunked_list = Enum.chunk_every(digits, pair_len)
      Enum.all?(chunked_list, fn element -> element == first end)
    end
    |> Enum.any?()
  end

  defp parse_input(input) do
    input
    |> String.split(",", trim: true)
    |> Enum.map(&String.split(&1, "-", trim: true))
    |> Enum.map(fn [l, r] ->
      l = String.to_integer(l)
      r = String.to_integer(r)
      Enum.to_list(l..r)
    end)
    |> List.flatten()
  end
end
2 Likes

Yes brute force is the way :smiley:

defmodule AdventOfCode.Solutions.Y25.Day02 do
  alias AoC.Input

  def parse(input, _part) do
    input
    |> Input.read!()
    |> String.trim()
    |> String.split(",")
    |> Enum.map(fn range ->
      [left, right] = String.split(range, "-")
      left = String.to_integer(left)
      right = String.to_integer(right)
      left..right
    end)
  end

  def part_one(problem) do
    problem
    |> Stream.flat_map(& &1)
    |> Stream.filter(&invalid_p1?/1)
    |> Enum.sum()
  end

  defp invalid_p1?(n) do
    mirror?(Integer.digits(n))
  end

  defp mirror?([a, a]), do: true
  defp mirror?([a, b, a, b]), do: true
  defp mirror?([a, b, c, a, b, c]), do: true
  defp mirror?([a, b, c, d, a, b, c, d]), do: true
  defp mirror?([a, b, c, d, e, a, b, c, d, e]), do: true
  defp mirror?(_), do: false

  def part_two(problem) do
    problem
    |> Stream.flat_map(& &1)
    |> Stream.filter(&invalid_p2?/1)
    |> Enum.sum()
  end

  defp invalid_p2?(n) do
    repeats?(Integer.digits(n))
  end

  defp repeats?([a, a]), do: true
  defp repeats?([a, a, a]), do: true
  defp repeats?([a, b, a, b]), do: true
  defp repeats?([a, a, a, a, a]), do: true
  defp repeats?([a, b, a, b, a, b]), do: true
  defp repeats?([a, b, c, a, b, c]), do: true
  defp repeats?([a, a, a, a, a, a, a]), do: true
  defp repeats?([a, b, a, b, a, b, a, b]), do: true
  defp repeats?([a, b, c, d, a, b, c, d]), do: true
  defp repeats?([a, a, a, a, a, a, a, a, a]), do: true
  defp repeats?([a, b, c, a, b, c, a, b, c]), do: true
  defp repeats?([a, b, a, b, a, b, a, b, a, b]), do: true
  defp repeats?([a, b, c, d, e, a, b, c, d, e]), do: true
  defp repeats?(_), do: false
end

Edit: I realize now that mirror? is a very incorrect name for that function :smiley:

5 Likes

Today is brute day:

# Parse
ranges =
  puzzle_input
  |> String.trim()
  |> String.split(",")
  |> Enum.map(fn range ->
    range
    |> String.split("-")
    |> Enum.map(&String.to_integer/1)
    |> then(&apply(Range, :new, &1))
  end)

# Impl
defmodule ElfRanges do
  def valid?(num) do
    len = floor(:math.log10(num)) + 1

    valid_n?(num, len, 2)
  end

  def valid_any?(num) do
    len = floor(:math.log10(num)) + 1

    Enum.all?(2..len//1, &valid_n?(num, len, &1))
  end

  def valid_n?(num, len, n) do
    if rem(len, n) == 0 do
      step = 10 ** div(len, n)

      Stream.unfold(num, fn
        0 -> nil
        val -> {rem(val, step), div(val, step)}
      end)
      |> Enum.dedup()
      |> then(&(not match?([_], &1)))
    else
      true
    end
  end
end

# P1
ranges
|> Stream.flat_map(& &1)
|> Stream.reject(&ElfRanges.valid?/1)
|> Enum.sum()

# P2
ranges
|> Stream.flat_map(& &1)
|> Stream.reject(&ElfRanges.valid_any?/1)
|> Enum.sum()
2 Likes

No regexes, no magic save for metaprogramming.

  defmodule H do
    def seq(i, count) do
      List.duplicate(
        {:"::", [], [{:seq, [], Elixir}, {:-, [], [{:binary, [], nil}, {:size, [], [i]}]}]},
        count
      )
    end
  end

  defmodule Day2 do
    @input "day2_1.input" |> File.read!() |> String.trim()

    Enum.each(1..100, fn i ->
      def forged_1(<<seq::binary-size(unquote(i)), seq::binary-size(unquote(i))>>), do: 1
    end)

    def forged_1(_), do: 0

    import Aoc2025.H

    for count <- 2..12, i <- 1..100 do
      def forged_2(<<unquote_splicing(seq(i, count))>>), do: 1
    end

    def forged_2(_), do: 0

    def calc(input \\ @input) do
      input
      |> String.split(",", trim: true)
      |> Stream.map(&String.split(&1, "-", trim: true))
      |> Stream.map(fn [b, e] -> String.to_integer(b)..String.to_integer(e) end)
      |> Stream.map(fn range -> Enum.reduce(range, 0, &(&2 + &1 * forged_2("#{&1}"))) end)
      |> Enum.sum()
    end
  end
3 Likes

Nice to see alternative solutions, as a first time use of Tasks I expected my naive version with async_stream to be faster than my slow solution, but it’s even slower. Why is that?

defmodule Advent2025Test do
  use ExUnit.Case

  def day2_data() do
    "day2.txt"
    |> File.stream!()
    |> Stream.flat_map(fn line -> String.split(String.trim(line), ",") end)
    |> Stream.map(fn line -> String.split(line, "-") end)
    |> Stream.flat_map(fn [from, to] ->
      Stream.map(String.to_integer(from)..String.to_integer(to), fn n -> n end)
    end)
  end

  def repeating_twice?(n) do
    ns = Integer.to_string(n)
    hl = Integer.floor_div(String.length(ns), 2)

    rem(String.length(ns), 2) == 0 and
      String.slice(ns, 0..(hl - 1)) == String.slice(ns, -hl..-1)
  end

  def repeating_any?(n) do
    x = Integer.to_string(n)
    String.length(x) > 1 and
      Enum.any?(
        Stream.map(0..(String.length(x) - 2), fn i ->
          Enum.all?(Stream.map(String.split(x, String.slice(x, 0..i)), fn x -> x == "" end))
        end)
      )
  end

  test "day2_p1" do
    sum = Enum.sum(day2_data() |> Stream.filter(fn n -> repeating_twice?(n) end))
    IO.puts("answer #{sum}")
    assert sum == 18_952_700_150
  end

  test "day2_p2_1" do
    sum = Enum.sum(day2_data() |> Stream.filter(fn n -> repeating_any?(n) end))
    IO.puts("answer #{sum}")
  end

  test "day2_p2" do
    res = day2_data() |> Task.async_stream(fn n -> if repeating_any?(n) do n else 0 end end)
    sum = Enum.sum_by(res, fn {:ok, num} -> num end)
    IO.puts("answer #{sum}")
  end
end
1 Like

I did this for part 1 too, but converted it to Integer.digits(num) and then chunking the resulting list into different sizes. Totally brute force but with some async goodies, part 2 runs in 0.4 seconds on my M1.

1 Like

I guess I did the brute force way too, as part 2 takes about 3 seconds. Who here has the more efficient solution? I can’t imagine what that might be.

defmodule RAoc.Solutions.Y25.Day02 do
  alias AoC.Input
  require Integer

  def parse(input, _part) do
    Input.read!(input)
    |> String.trim()
    |> String.split(",")
    |> Enum.map(&String.split(&1, "-"))
    |> Enum.map(fn [str1, str2] -> String.to_integer(str1)..String.to_integer(str2) end)
    |> Stream.flat_map(fn range -> range end)
  end

  def part_one(problem) do
    problem
    |> Stream.filter(&is_invalid_code_part1?/1)
    |> Enum.sum()
  end

  defp is_invalid_code_part1?(code) do
    str_code = Integer.to_string(code)
    len = String.length(str_code)
    (Integer.is_even(len) && is_repeated_pattern?(str_code, len, 2)) || false
  end

  def part_two(problem) do
    problem
    |> Stream.filter(&is_invalid_code_part2?/1)
    |> Enum.sum()
  end

  defp is_invalid_code_part2?(code) do
    str_code = Integer.to_string(code)
    len = String.length(str_code)

    Enum.filter(2..len//1, fn n -> is_divisible?(len, n) end)
    |> Enum.any?(fn num_parts ->
      is_repeated_pattern?(str_code, len, num_parts)
    end)
  end

  # Because of previous checks, len is divisible by num_parts, this function doesn't check that
  defp is_repeated_pattern?(str_code, len, num_parts) do
    split_into_parts(str_code, [], div(len, num_parts))
    |> Enum.uniq()
    |> Enum.count() == 1
  end

  defp is_divisible?(n, m), do: div(n, m) * m == n

  defp split_into_parts("", acc, _at) do
    acc
  end

  defp split_into_parts(str, acc, at) do
    {str1, str2} = String.split_at(str, at)
    split_into_parts(str2, [str1 | acc], at)
  end
end
1 Like

Yup, this is way faster than mine!

Edit: This is a great example of how fast and efficient pattern matching must be inside the BEAM.

1 Like

Ever heard of metaprogramming? :slight_smile: Mine goes up to 12 repetitions and then pattern matches the string which should be technically faster (but I am not sure.)

1 Like

Yeah of course, but I was in rush so actually I just wrote a loop to print the clauses and then pasted them into the module. Quick and dirty :smiley:

Edit: well that actually counts as metaprogramming :smiley:

3 Likes
defmodule Ben.Day02 do
  @moduledoc ~S"""
  # Gift Shop

  Identify the invalid product IDs
  """

  @doc ~S"""

  ### Example data

  11-22,95-115,998-1012,1188511880-1188511890,222220-222224, 1698522-1698528,446443-446449,38593856-38593862,565653-565659, 824824821-824824827,2121212118-2121212124

  - The ranges are separated by commas (,)
  - each range gives its first ID and last ID separated by a dash (-).

  We return an integer indicating the sum of all the invalid IDs found with a 'mirror' sequence like 1010 or 11 or 858858."

    iex> part_one("priv/data/ben/day_02_example_data.txt")
    1_227_775_554

  """
  @spec part_one(String.t()) :: non_neg_integer
  def part_one("priv/" <> _rest_of_path = path_to_data) do
    path_to_data
    |> stream_input()
    |> Enum.reduce([], fn
      range, acc ->
        [Enum.filter(range, &evenly_repeated_sequence_of_digits?/1) | acc]
    end)
    |> List.flatten()
    |> Enum.sum()
  end

  defp evenly_repeated_sequence_of_digits?(digit) do
    half_digit_length = div(digit_length(digit), 2)
    divisor_and_modulo = trunc(:math.pow(10, half_digit_length))

    digit_of_even_length?(digit) &&
      # this compares left half of digit with even length of digits
      # to is right half of digits
      # no need to convert digit to any other type
      div(digit, divisor_and_modulo) == rem(digit, divisor_and_modulo)
  end

  defp digit_of_even_length?(digit), do: rem(digit_length(digit), 2) == 0
  defp digit_length(digit), do: trunc(:math.log10(digit) + 1)

  # ------------------------------------------------------------------------------
  @doc ~S"""
  Now, an ID is invalid if it is made only of some sequence of digits repeated at least twice. So, 12341234 (1234 two times), 123123123 (123 three times), 1212121212 (12 five times), and 1111111 (1 seven times) are all invalid IDs.

    iex> part_two("priv/data/ben/day_02_example_data.txt")
    4_174_379_265
  """
  @spec part_two(String.t()) :: non_neg_integer
  def part_two("priv/" <> _rest_of_path = path_to_data) do
    path_to_data
    |> stream_input()
    |> Enum.reduce([], fn
      range, acc ->
        [Enum.filter(range, &evenly_repeated_or_entirely_repeated_sequence_of_digits?/1) | acc]
    end)
    |> List.flatten()
    |> Enum.sum()
  end

  defp evenly_repeated_or_entirely_repeated_sequence_of_digits?(digit),
    do: evenly_repeated_sequence_of_digits?(digit) || repeated_sequence_of_digits?(digit)

  defp repeated_sequence_of_digits?(digit),
    do:
      digit
      |> Integer.to_string()
      # Starts with any number except 0
      # Has a repeating pattern of digits from [0-9]
      # Must end in a digit
      |> String.match?(~r/^([1-9]+[0-9]*)\1+$/)

  # ------------------------------------------------------------------------------

  defp stream_input(input) do
    File.read!(input)
    |> String.trim()
    |> Stream.unfold(&next_word/1)
    |> Stream.map(&String.split(&1, "-"))
    |> Stream.map(fn [left, right] -> String.to_integer(left)..String.to_integer(right) end)
  end

  # The next_function for Stream.unfold
  defp next_word("") do
    # When the accumulator (remaining string) is empty, stop the stream
    nil
  end

  defp next_word(remaining_string) do
    case String.split(remaining_string, ",", parts: 2) do
      [word, rest] ->
        # Yield the word, and use the 'rest' as the next accumulator
        {word, rest}

      [word] ->
        # Yield the final word, and use an empty string "" as the next accumulator
        # which will cause the next call to return nil and stop the stream.
        {word, ""}
    end
  end
end

1 Like

Got the answer, then went on to rewrite it faster…

Test with example data: Passed, what can go wrong?! :slight_smile:
Final result: slightly too low :frowning:

For those wanna figure out my mistake:

part_one: 41294979841 in 286.98ms
part_two: 66497614013 in 280.2ms

defmodule Aoc2025.Solutions.Y25.Day02 do
  alias AoC.Input

  @max_id_length 10
  @max_args Integer.floor_div(@max_id_length, 2)

  def parse(input, _part) do
    Input.read!(input)
    |> String.trim()
    |> String.split(",")
  end

  def part_one(problem), do: solve(problem, :p1)
  def part_two(problem), do: solve(problem, :p2)

  def solve(problem, mode) do
    problem
    |> Stream.map(fn x -> String.split(x, "-") end)
    |> Stream.map(fn [x, y] -> String.to_integer(x)..String.to_integer(y) end)
    |> Enum.flat_map(&check_range(&1, mode))
    |> Enum.sum()
  end

  def check_range(range, mode), do: Enum.map(range, &check_id(mode, Integer.digits(&1)))

  # Some macro programming
  vars = Macro.generate_unique_arguments(@max_args, __MODULE__)
  var_sets = Enum.map(1..@max_args, &Enum.take(vars, &1))

  for set <- var_sets do
    superset = set ++ set
    superset1 = set ++ set ++ set
    superset2 = set ++ set ++ set ++ set
    superset3 = set ++ set ++ set ++ set ++ set
    def check_id(_, unquote(superset) = idl), do: Integer.undigits(idl)
    def check_id(:p2, unquote(superset1) = idl), do: Integer.undigits(idl)
    def check_id(:p2, unquote(superset2) = idl), do: Integer.undigits(idl)
    def check_id(:p2, unquote(superset3) = idl), do: Integer.undigits(idl)
  end

  def check_id(_, _), do: 0
end

Note to self: examine the whole input file before writing code (I keep using the example data for it, but miss some tricks and optimizations)

1 Like

Tried to make it understandable.

defmodule AdventOfCode.Solution.Year2025.Day02 do
  import Enum, only: [map: 2, chunk_every: 2, any?: 1, sum: 1]

  def part1(input), do: scan(input, 2)
  def part2(input), do: scan(input, :number_length)

  def scan(input, stop_at) do
    sum(for {l, h} <- parse(input), n <- l..h, is_invalid(n, stop_at), do: n)
  end

  def parse(input) do
    input
    |> String.trim()
    |> String.split(",", trim: true)
    |> map(fn r ->
      [l, h] = String.split(r, "-")
      {String.to_integer(l), String.to_integer(h)}
    end)
  end

  def is_invalid(n, _stop_at) when n <= 9, do: false

  def is_invalid(n, stop_at) do
    n_into_chars = n |> to_string() |> to_charlist()
    number_length = length(n_into_chars)
    test_until = if stop_at == :number_length, do: number_length, else: stop_at

    # Cut into all sizes of equal pieces (length must be divisible into equal size)
    for i <- 2..test_until, rem(number_length, i) == 0 do
      n_into_chars |> chunk_every(div(number_length, i)) |> uniform_list?()
    end
    |> any?()
  end

  # Alternative: def uniform_list?(l), do: length(Enum.uniq(l)) == 1
  def uniform_list?([_a]), do: true
  def uniform_list?([a, b | _r]) when a != b, do: false
  def uniform_list?([_a, b | r]), do: uniform_list?([b | r])
end

2 Likes

look, ma - no brute force!

defmodule Y2025.Day02 do
  def digits(a) do
    trunc(:math.floor(:math.log10(a))) + 1
  end

  def pow10(n), do: trunc(:math.pow(10, n))

  def multiplier(d, 2), do: pow10(d) + 1
  def multiplier(d, n), do: 1 + pow10(d) * multiplier(d, n - 1)

  def start_and_multiplier(a, n) do
    dig = digits(a)
    {d, m} = {div(dig, n), Integer.mod(dig, n)}

    if m == 0 do
      {div(a, pow10(dig - d)), multiplier(d, n)}
    else
      {pow10(d), multiplier(d + 1, n)}
    end
  end

  def invalid_ids(a, b, n) do
    {start, mult} = start_and_multiplier(a, n)
    max_start = pow10(digits(start))

    start..(max_start - 1)
    |> Enum.reduce_while([], fn i, acc ->
      x = i * mult

      cond do
        x < a -> {:cont, acc}
        x > b -> {:halt, Enum.reverse(acc)}
        true -> {:cont, [x | acc]}
      end
    end)
  end

  def parse(s) do
    String.split(s, ",")
    |> Enum.map(fn s ->
      [a, b] = String.split(s, "-")
      [String.to_integer(a), String.to_integer(b)]
    end)
  end

  def part1(s) do
    parse(s)
    |> Enum.flat_map(fn [a, b] -> invalid_ids(a, b, 2) end)
    |> Enum.sum()
  end

  def all_invalid_ids(a, b) do
    2..max(digits(b), 2)
    |> Enum.flat_map(fn i -> invalid_ids(a, b, i) end)
    |> Enum.uniq()
  end

  def part2(s) do
    parse(s)
    |> Enum.flat_map(fn [a, b] -> all_invalid_ids(a, b) end)
    |> Enum.sum()
  end
end
3 Likes

Nice, this is the fastest solution I’ve tried. I’m just trying to wrap my head around the math in your start_and_multiplier function

1 Like

The idea is simple, and best illustrated with a few examples:

iex(2)>  start_and_multiplier(998,2)
{10, 101}
iex(3)>  start_and_multiplier(11,2)
{1, 11}
iex(4)>  start_and_multiplier(12345,3)
{10, 10101}
iex(5)>  start_and_multiplier(12345678,4)
{12, 1010101}

First start with an observation that repeating a 2 digits number, say 12, three times is the same as multiplying it by 10101. That’s the multiplier part.

The start part is a bit trickier: if the digits of the number a can be divided neatly into n groups of digits, it’s the first such group:

iex(7)>  start_and_multiplier(123456, 3)
{12, 10101}
iex(8)>  start_and_multiplier(123456, 2)
{123, 1001}

Otherwise, we take the first div(# digits in a, n) digits and take the next power of 10 greater than that number, so e.g., 9 → 10, 23 → 100, etc. The reason is that for numbers like 998, the digits cannot be split evenly in two groups, so the first number we should start with is 1010 = 10 * 101.

1 Like

And so the algorithm is, essentially: invalid ids are all the numbers from [ start * multiplier, (start + 1) * multiplier, (start + 2) * multiplier, ...] that belong to a..b. There is a trick to terminate the loop early, when incrementing start increases the number of digits, at which point multiplier won’t work anymore, and we implicitly use the fact that the difference between a and b is not too large - my algorithm won’t work for a = 10, b = 100000, for example.

2 Likes