Advent Of Code 2022 - Day 4

Took me a minute to remember my binary math :smile: :grimacing:

import Bitwise

__DIR__
|> Path.join("puzzle.txt")
|> File.stream!()
|> Stream.filter(fn line ->
  line
  |> String.trim()
  |> String.split(",")
  |> Enum.map(fn range ->
    [s,e] = range |> String.split("-") |> Enum.map(& String.to_integer(&1))
    Integer.pow(2, e - s + 1) - 1 <<< (s - 1)
  end)
  |> Enum.reduce(fn int1, int2 ->
    intersection = int1 ||| int2
    intersection == int1 or intersection == int2
  end)
end)
|> Enum.count()
|> IO.puts()
import Bitwise

__DIR__
|> Path.join("puzzle.txt")
|> File.stream!()
|> Stream.filter(fn line ->
  line
  |> String.trim()
  |> String.split(",")
  |> Enum.map(fn range ->
    [s,e] = range |> String.split("-") |> Enum.map(& String.to_integer(&1))
    Integer.pow(2, e - s + 1) - 1 <<< (s - 1)
  end)
  |> Enum.reduce(fn int1, int2 ->
    (int1 &&& int2) != 0
  end)
end)
|> Enum.count()
|> IO.puts()

Edit: oops, I’m supposed to import not use Bitwise now…

2 Likes

I am grateful for MapSet module. Not every language is blessed with a set API that has common set operations.

Second half was actually a little simpler.

# Part 1
day_4_data
|> String.split("\n")
|> Enum.map(fn ranges -> 
   ranges
   |> String.split(",")
   |> Enum.map(fn range -> 
        [start, finish] = String.split(range, "-")
        (String.to_integer(start)..String.to_integer(finish)) |> MapSet.new()
      end)
   end)
|> Enum.count(fn [first, second] -> 
     MapSet.subset?(first, second) || MapSet.subset?(second, first)
   end)

# Part 2
day_4_data
|> String.split("\n")
|> Enum.map(fn ranges -> 
     ranges
     |> String.split(",")
     |> Enum.map(fn range -> 
          [start, finish] = String.split(range, "-")          
          (String.to_integer(start)..String.to_integer(finish))
        end)
   end)
|> Enum.count(fn [first, second] -> not Range.disjoint?(first, second) end)
2 Likes

Basically did the same. I wasn’t as clever with your use of the && and || operators to convert to numbers. I just filtered and counted.

1 Like

Like others, using Range

4 Likes

Ooh, TIL, I started with Ranges, but then converted them to MapSets - I didn’t consider that we can do it, especially part 2, directly with the range. This is why AoC is great :slight_smile:

  def part1(input) do
    Enum.count(input, fn [a, b] -> MapSet.subset?(a, b) or MapSet.subset?(b, a) end)
  end

  def part2(input) do
    Enum.count(input, fn [a, b] -> not MapSet.disjoint?(a, b) end)
  end

  def input do
    with [input_filename] <- System.argv(),
         {:ok, input} <- File.read(input_filename) do
      input
      |> String.split(["-", ",", "\n"], trim: true)
      |> Enum.map(&String.to_integer/1)
      |> Enum.chunk_every(2)
      |> Enum.map(fn [a, b] -> MapSet.new(a..b) end)
      |> Enum.chunk_every(2)
    else
      _ -> :error
    end
  end
2 Likes

part 1

start = System.monotonic_time(:microsecond)

File.stream!("input.txt")
|> Stream.map(fn line ->
   Regex.run(~r/^(\d+)-(\d+),(\d+)-(\d+)$/, line, capture: :all_but_first)
   |> Enum.map(&String.to_integer/1)
end)
|> Enum.reduce(0, fn [lo1, hi1, lo2, hi2], count ->
  cond do
    lo1 >= lo2 && hi1 <= hi2 -> count + 1
    lo2 >= lo1 && hi2 <= hi1 -> count + 1
    true -> count
  end
end)
|> tap(fn count -> IO.puts "Number of contained assignment pairs: #{count}" end)

elapsed = System.monotonic_time(:microsecond) - start
IO.puts "Job done in #{elapsed} µs"

part 2 is very similar to part 1.

start = System.monotonic_time(:microsecond)

File.stream!("input.txt")
|> Stream.map(fn line ->
   Regex.run(~r/^(\d+)-(\d+),(\d+)-(\d+)$/, line, capture: :all_but_first)
   |> Enum.map(&String.to_integer/1)
end)
|> Enum.reduce(0, fn [lo1, hi1, lo2, hi2], count ->
  cond do
    lo1 >= lo2 && lo1 <= hi2 -> count + 1
    lo2 >= lo1 && lo2 <= hi1 -> count + 1
    hi1 >= lo2 && hi1 <= hi2 -> count + 1
    hi2 >= lo1 && hi2 <= hi1 -> count + 1
    true -> count
  end
end)
|> tap(fn count -> IO.puts "Number of overlapping assignment pairs: #{count}" end)

elapsed = System.monotonic_time(:microsecond) - start
IO.puts "Job done in #{elapsed} µs"
...
|> Enum.map(& &1 |> String.replace("-", "..") |> Code.eval_string() |> elem(0))

and then

|> Enum.map(fn [r1, r2] -> not Range.disjoint?(r1, r2) end)
4 Likes

clever parsing :sweat_smile:

I’ve been working through AOC using Gleam this year! This is probably pretty terrible Gleam code, but it’s Elixir-adjacent so I thought I’d share :slight_smile:

1 Like

I also went with parsing into Ranges, and was also surprised to learn about Range.disjoint?/2 solving part two that literally did all the work compared to part one.

What I really found impressive is that disjoint? handles steps with ranges, as well! That’s a cool technique.

I used ranges as well. Part 1 and 2 are basically contains? and overlaps? for ranges. Did the first on my own, for part 2 Range.disjoint? is the opposite to overlaps?, so it does the job. Contains can be checked just by looking at the edges of the ranges.

Solution
defmodule Day4 do
  def sum_number_of_contained_assignments(text) do
    text
    |> String.split("\n")
    |> Enum.reject(&(&1 == ""))
    |> Enum.filter(&contained_assignments?/1)
    |> length()
  end

  def sum_number_of_overlapping_assignments(text) do
    text
    |> String.split("\n")
    |> Enum.reject(&(&1 == ""))
    |> Enum.filter(&overlapping_assignments?/1)
    |> length()
  end

  defp contained_assignments?(assignments) do
    {elf_a_range, elf_b_range} = parse_pair_ranges(assignments)
    contains?(elf_a_range, elf_b_range) or contains?(elf_b_range, elf_a_range)
  end

  defp overlapping_assignments?(assignments) do
    {elf_a_range, elf_b_range} = parse_pair_ranges(assignments)
    !Range.disjoint?(elf_a_range, elf_b_range)
  end

  defp parse_pair_ranges(assignments) do
    [elf_a_assignment, elf_b_assignment] = String.split(assignments, ",", trim: true, parts: 2)
    elf_a_range = to_range(elf_a_assignment)
    elf_b_range = to_range(elf_b_assignment)
    {elf_a_range, elf_b_range}
  end

  defp to_range(assignment) do
    [from, to] = String.split(assignment, "-", trim: true, parts: 2)
    from_int = String.to_integer(from)
    to_int = String.to_integer(to)
    from_int..to_int//1
  end

  defp contains?(fa..ta//1, fb..tb//1) do
    fa <= fb and ta >= tb
  end
end

My simple solution:

defmodule Day04 do
  def part1(input_path) do
    input_path
    |> File.stream!()
    |> Stream.map(&String.trim/1)
    |> Stream.map(&String.split(&1, ~r/\D/, global: true))
    |> Stream.map(fn x -> Enum.map(x, &String.to_integer/1) end)
    |> Enum.count(fn [start1, end1, start2, end2] ->
      (start1 <= start2 and end1 >= end2) or
      (start1 >= start2 and end1 <= end2)
    end)
  end

  def part2(input_path) do
    input_path
    |> File.stream!()
    |> Stream.map(&String.trim/1)
    |> Stream.map(&String.split(&1, ~r/\D/, global: true))
    |> Stream.map(fn x -> Enum.map(x, &String.to_integer/1) end)
    |> Enum.count(fn [start1, end1, start2, end2] ->
      start1 <= end2 and start2 <= end1
    end)
  end
end
4 Likes

Advent of code always reminds me what a great “stdlib” Elixir comes with!

  parsed = 
    input
    |> String.split("\n", trim: true)
    |> Enum.flat_map(&String.split(&1, ~r/[-,]/))
    |> Enum.map(&String.to_integer/1)
    |> Enum.chunk_every(4)

part1 = Enum.count(parsed, fn [ax, ay, bx, by] -> (ax <= bx and ay >= by) or (ax >= bx and ay <= by) end)
part2 = Enum.count(parsed, fn [ax, ay, bx, by] -> (bx <= ay and by >= ax) end)

Edit: @Aetherus you beat me by a few seconds :wink:

4 Likes

I am getting the feeling that Advent of Code is written from the mindset of primarily imperative languages, because it feels they’re getting easier with Elixir. :slight_smile: Today’s was just Range and MapSet doing everything. I think today’s is probably the closest to literally just restating the problem description in Elixir. Part two required no edits to the code to solve part one since it just required a different count function. Link to Livebook notebook.

defmodule Day4 do
  @moduledoc """
  Solutions for Day 4
  """

  @typedoc """
  Represents a pairing of two elf's section assignments. The section assignments
  are represented by Elixir `Range`s.
  """
  @type assignment_pair() :: {Range.t(), Range.t()}

  @doc """
  Parse an assignment pair string into a tuple of section assignment ranges

  ## Examples:
    iex> Day4.parse_assignment_pair("2-4,6-8")
    {2..4, 6..8}
  """
  @spec parse_assignment_pair(String.t()) :: assignment_pair()
  def parse_assignment_pair(string) do
    [[a_start, a_end], [b_start, b_end]] =
      for range <- String.split(string, ",", trim: true) do
        String.split(range, "-", trim: true)
        |> Enum.map(&String.to_integer/1)
      end

    {Range.new(a_start, a_end), Range.new(b_start, b_end)}
  end

  @doc """
  List of all the assignment pairs
  """
  @spec assignment_pairs() :: [assignment_pair()]
  def assignment_pairs() do
    Utilities.read_data(4)
    |> Enum.map(&parse_assignment_pair/1)
  end

  @doc """
  Determines if range 1 is a subset of range 2
  """
  @spec range_subset?(Range.t(), Range.t()) :: boolean()
  def range_subset?(range1, range2) do
    MapSet.subset?(MapSet.new(range1), MapSet.new(range2))
  end

  @doc """
  Determines if one of the ranges is fully contained in (i.e., a subset of) the other
  """
  @spec range_contained_in_the_other?(Range.t(), Range.t()) :: boolean()
  def range_contained_in_the_other?(range1, range2) do
    range_subset?(range1, range2) or range_subset?(range2, range1)
  end

  def part_one() do
    assignment_pairs()
    |> Enum.count(fn {range1, range2} -> range_contained_in_the_other?(range1, range2) end)
  end

  def part_two() do
    assignment_pairs()
    |> Enum.count(fn {range1, range2} -> !Range.disjoint?(range1, range2) end)
  end
end

Probably not the most performant code but it does the job. Suggestions are appreciated

One more day for scanning

defmodule AOC do
  def traverse(string, number \\ 0, line \\ [], acc \\ 0)
  def traverse("", 0, [], acc), do: acc
  def traverse(string, 0, [r2, l2, r1, l1], acc) do
    cond do
      r1 < l2 ->
        traverse(string, 0, [], acc)

      r2 < l1 ->
        traverse(string, 0, [], acc)

      true ->
        traverse(string, 0, [], acc + 1)
    end
  end
  def traverse(string, number, line, acc) do
    case string do
      <<splitter, tail :: binary>> when splitter in [?-, ?,, ?\n] ->
        traverse(tail, 0, [number | line], acc)

      <<int, tail :: binary>> ->
        traverse(tail, number * 10 + int - ?0, line, acc)

      "" ->
        traverse("", 0, [number | line], acc)

      "\n" ->
        traverse("", 0, [number | line], acc)
    end
  end
end

IO.inspect AOC.traverse IO.read :eof
1 Like

day04.exs:

solve = fn oper ->
  "data/04"
  |> File.read!()
  |> String.split(["\n", ",", "-"], trim: true)
  |> Enum.map(&String.to_integer/1)
  |> Enum.chunk_every(4)
  |> Enum.filter(fn [a, b, c, d] -> oper.(c in a..b, d in a..b) or oper.(a in c..d, b in c..d) end)
  |> Enum.count()
end

(&and/2)
|> then(solve)
|> tap(&IO.puts("Part 1: #{&1}"))

(&or/2)
|> then(solve)
|> tap(&IO.puts("Part 2: #{&1}"))
4 Likes

Part 1

input
|> String.split("\n", trim: true)
|> Enum.map(fn line ->
  %{"a1" => a1, "a2" => a2, "b1" => b1, "b2" => b2} =
    Regex.named_captures(~r/(?<a1>\d+)-(?<a2>\d+),(?<b1>\d+)-(?<b2>\d+)/, line)
  {Range.new(String.to_integer(a1), String.to_integer(a2)), Range.new(String.to_integer(b1), String.to_integer(b2))}
end)
|> Enum.filter(fn {r1, r2} ->
  s1 = MapSet.new(r1)
  s2 = MapSet.new(r2)

  MapSet.subset?(s1, s2) || MapSet.subset?(s2, s1)
end)
|> length()

Part 2

input
|> String.split("\n", trim: true)
|> Enum.map(fn line ->
  %{"a1" => a1, "a2" => a2, "b1" => b1, "b2" => b2} =
    Regex.named_captures(~r/(?<a1>\d+)-(?<a2>\d+),(?<b1>\d+)-(?<b2>\d+)/, line)
  {Range.new(String.to_integer(a1), String.to_integer(a2)), Range.new(String.to_integer(b1), String.to_integer(b2))}
end)
|> Enum.reject(fn {r1, r2} ->
  s1 = MapSet.new(r1)
  s2 = MapSet.new(r2)

  MapSet.disjoint?(s1, s2)
end)
|> length()