Advent of Code 2025 - Day 2

defmodule Day2 do
  def file, do: Parser.read_file(2)
  def test, do: Parser.read_file("test")

  def parse(input) do
    input
    |> Enum.join()
    |> String.split(",")
    |> Enum.map(&(&1 |> String.split("-") |> Enum.map(fn a -> String.to_integer(a) end)))
  end

  def solve(input \\ file()) do
    input
    |> parse()
    |> Enum.reduce([], fn range, acc ->
      range
      |> find_invalid_ids_in_range(&is_symmetric?/1)
      |> Enum.concat(acc)
    end)
    |> Enum.sum()
  end

  def solve_two(input \\ file()) do
    input
    |> parse()
    |> Enum.reduce([], fn range, acc ->
      range
      |> find_invalid_ids_in_range(&contain_pattern?/1)
      |> Enum.concat(acc)
    end)
    |> Enum.sum()
  end

  def find_invalid_ids_in_range([alpha, omega], filter_function) do
    Enum.filter(alpha..omega, &filter_function.(&1))
  end

  def is_symmetric?(number) do
    string = Integer.to_string(number)
    length = String.length(string)
    {a, b} = String.split_at(string, (length / 2) |> trunc())
    a == b
  end

  def contain_pattern?(number) do
    string = Integer.to_string(number)
    length = String.length(string)

    if length == 1 do
      false
    else
      divide_by = Enum.filter(2..length, fn a -> rem(length, a) == 0 end)
      Enum.any?(divide_by, &is_invalid_multiple(string, length, &1))
    end
  end

  def is_invalid_multiple(string, length, divide_by) do
    slice_to = trunc(length / divide_by) - 1
    pattern = string |> String.slice(0..slice_to)
    String.duplicate(pattern, divide_by) == string
  end
end
#!/usr/bin/env elixir

# Advent of Code 2025. Day 2

Mix.install([
  {:nimble_parsec, "~> 1.4.2"},
])

defmodule M do
  import NimbleParsec

  ranges = repeat(
    integer(min: 1)
    |> ignore(string("-"))
    |> integer(min: 1)
    |> tag(:range)
    |> optional(ignore(string(",")))
  )

  defparsec :ranges, ranges

  def invalid1(x) do
    x = to_string(x)
    {a,b} = String.split_at(x, div(String.length(x),2))
    a == b
  end

  def invalid2(x) do
    String.match?(to_string(x), ~r/^(\d+?)\1+$/)
  end
end

ranges = File.read!("../day02.txt")
  |> M.ranges()
  |> then(fn {:ok, lst, "\n", %{}, _, _} -> Enum.map(lst, fn {:range, [lo, hi]} -> (lo..hi) end) end)

# Part 1
ranges
|> Enum.flat_map(fn range -> Enum.filter(range, &M.invalid1/1) end)
|> Enum.sum()
|> IO.inspect(label: "Day 02. Part 1")

# Part 2
ranges
|> Enum.flat_map(fn range -> Enum.filter(range, &M.invalid2/1) end)
|> Enum.sum()
|> IO.inspect(label: "Day 02. Part 2")

Probably the most interesting thing I did differently was using binary comprehensions to split the string into equal parts:

def repeats?(string, parts \\ 2)
def repeats?(string, parts) when parts > byte_size(string), do: false

def repeats?(string, parts) do
  repeats? =
    if rem(byte_size(string), parts) > 0 do
      # Can't repeat if not divisible
      false
    else
      part_len = div(byte_size(string), parts)
      parts = for <<part::binary-size(part_len) <- string>>, do: part
      parts |> Enum.uniq() |> length() == 1
    end

  repeats? or repeats?(string, parts + 1)
end

Finally getting back to the puzzles…

defmodule Day02 do
  def part1(file), do: main(file, &invalid1?/1)
  def part2(file), do: main(file, &invalid2?/1)

  def main(file, filter) do
    rs = file |> File.read!() |> String.replace("\n", "") |> String.split(",") |> Enum.map(&rng/1)

    rs
    |> Enum.flat_map(fn range -> Enum.filter(range, &(&1 |> Integer.digits() |> filter.())) end)
    |> Enum.sum()
  end

  def rng(string) do
    [first, last] = string |> String.split("-") |> Enum.map(&(&1 |> Integer.parse() |> elem(0)))
    Range.new(first, last)
  end

  def invalid1?(digits) do
    len = length(digits)
    rem(len, 2) == 0 and repeats?(digits, div(len, 2))
  end

  def invalid2?(digits) do
    len = length(digits)
    len > 1 and Enum.any?(1..max(div(len, 2), 1), &(rem(len, &1) == 0 and repeats?(digits, &1)))
  end

  def repeats?(digits, size) do
    [chunk | rest] = Enum.chunk_every(digits, size)
    Enum.all?(rest, &(&1 == chunk))
  end
end