Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Advent of Code 2022

2022/elixir/advent_of_code_2022.livemd

Advent of Code 2022

Introduction

These are solutions for Advent of Code 2022 written in Elixir. Elixir Livebook is utilized to create this notebook.

The overall strategy is domain-driven design, in that each puzzle is modeled as a domain using Elixir types and functions. The intent is not to provide “clever” solutions that often utilize some sort of computational trickery but to provide a clear linear path from problem statement, a domain model of the problem, an implementation of that model, and then ultimately a solution. It is considered a failure if the solution does not essentially read like an Elixir encoding of the problem description.

The following are provided:

  • Full solutions for each day
  • Documented modules, types, and functions
  • Typespecs for every type and function
  • Tests to help verify refactors that occur after a correct solution is arrived at

It is for these reasons that the solutions are somewhat “verbose”, if one really wants to use that term. I don’t necessarily because I view these solutions are the minimum form required to provided the necessary function, a perspective of form equals function. The solutions provided here are fully industrialized and tend to be quite amenable to refactors, often making the transition from part one to part two quite nice and straightforward.

For the most part, solutions are contained within a single Day module. Solution values for the two parts are provided as functions &part_one/0 and &part_two/0. However, some days may drastically alter the solution from part one. In cases such as those, two separate modules for the day, such as Day.PartOne and Day.PartTwo. Solution values in this case are provided as &solution/0 in the respective module.

Contents

Utilities

defmodule Utilities do
  @moduledoc """
  Provides utility functions to be used across days
  """

  @doc """
  Reads the given day's data file of "day__input.txt" as
  a stream
  """
  @spec read_data(integer(), keyword()) :: Stream.t()
  def read_data(day, opts \\ [trim: true]) do
    day_as_string =
      day
      |> Integer.to_string()
      |> String.pad_leading(2, "0")

    lines =
      Path.join(__DIR__, "../data/day_#{day_as_string}_input.txt")
      |> Path.expand()
      |> File.stream!()

    if opts[:trim] do
      Stream.map(lines, &String.trim/1)
    else
      lines
    end
  end

  @doc """
  Tests whether the entire string is uppercase
  """
  @spec uppercase?(String.t()) :: boolean()
  def uppercase?(string), do: string == String.upcase(string)

  @doc """
  Tests whether the entire string is lowercase
  """
  @spec lowercase?(String.t()) :: boolean()
  def lowercase?(string), do: string == String.downcase(string)

  @doc """
  Transposes the given rows. If the rows are not all of the same length, then
  the lengths are normalized to the shortest row.

  ## Examples:
    iex> transpose([1, 2, 3], [4, 5, 6])
    [[1, 2], [3, 4], [5, 6]]
  """
  @spec transpose([any()]) :: [any()]
  def transpose(rows) do
    rows
    |> List.zip()
    |> Enum.map(&Tuple.to_list/1)
  end

  @doc """
  Tests if the string is the empty string

  ## Examples:
    iex> empty?("test")
    false

    iex> empty?("")
    true
  """
  @spec empty?(String.t()) :: boolean()
  def empty?(string) do
    string == ""
  end

  @doc """
  Tests if the string is not the empty string

  ## Examples:
    iex> non_empty?("test")
    true

    iex> non_empty?("")
    false
  """
  @spec non_empty?(String.t()) :: boolean()
  def non_empty?(string) do
    string != ""
  end

  @doc """
  Drops the last element of the enum
  """
  @spec drop_last(Enum.t()) :: Enum.t()
  def drop_last(enum) do
    enum
    |> Enum.reverse()
    |> tl()
    |> Enum.reverse()
  end

  @doc """
  Tests if all the elements of the enumerable are unique
  """
  @spec unique_elements?(Enum.t()) :: boolean()
  def unique_elements?(enum) do
    length(enum) == length(Enum.uniq(enum))
  end
end
{:module, Utilities, <<70, 79, 82, 49, 0, 0, 20, ...>>, {:unique_elements?, 1}}
defmodule Stack do
  @moduledoc """
  A stack data structure
  """

  @enforce_keys [:elements]
  defstruct elements: []

  @typedoc """
  Represents a stack where the given `element_type` parameter is the type of the
  stack's elements
  """
  @type t(element_type) :: %__MODULE__{
          elements: list(element_type)
        }

  @doc """
  Create a new stack from the given elements. The order of the list is preserved
  such that the head of the given element list becomes the top of the stack.
  """
  @spec new([stack_type]) :: __MODULE__.t(stack_type) when stack_type: any()
  def new(initial_elements) do
    %__MODULE__{
      elements: initial_elements
    }
  end

  @doc """
  Push the element onto the top of the stack and return the stack
  """
  @spec push(__MODULE__.t(stack_type), stack_type) :: __MODULE__.t(stack_type)
        when stack_type: any()
  def push(%__MODULE__{elements: elements}, element) do
    new([element | elements])
  end

  @doc """
  Push the elements onto the top of the stack and return the stack. The elements
  are not pushed one at a time (thus reversing their order from the order as given)
  but are instead pushed all at once as a group (the opposite order from if they were pushed
  one at a time).
  """
  @spec push_as_group(__MODULE__.t(stack_type), [stack_type]) :: __MODULE__.t(stack_type)
        when stack_type: any()
  def push_as_group(%__MODULE__{elements: elements}, new_elements) do
    new(new_elements ++ elements)
  end

  @doc """
  Pops the element at the top of the stack off of the stack and returns a tuple
  consisting of the element and the remaining stack.
  """
  @spec pop(__MODULE__.t(stack_type)) :: {stack_type, __MODULE__.t(stack_type)}
        when stack_type: any()
  def pop(%__MODULE__{elements: [head | tail]}) do
    {head, new(tail)}
  end

  @doc """
  Peeks the element at the top of the stack and returns it
  """
  @spec peek(__MODULE__.t(stack_type)) :: stack_type when stack_type: any()
  def peek(%__MODULE__{elements: [head | _tail]}) do
    head
  end

  @doc """
  Peeks the number of elements at the top of the stack and returns them
  """
  @spec peek(__MODULE__.t(stack_type), integer()) :: stack_type when stack_type: any()
  def peek(%__MODULE__{elements: elements}, n) do
    Enum.take(elements, n)
  end

  @doc """
  Drops the element at the top of the stack and returns the remaining stack
  """
  @spec drop(__MODULE__.t(stack_type)) :: __MODULE__.t(stack_type) when stack_type: any()
  def drop(%__MODULE__{elements: [_head | tail]}) do
    new(tail)
  end

  @doc """
  Drops the number of elements on top of the stack and returns the remaining stack
  """
  @spec drop(__MODULE__.t(stack_type), integer()) :: __MODULE__.t(stack_type)
        when stack_type: any()
  def drop(%__MODULE__{elements: elements}, n) do
    elements_left = Enum.drop(elements, n)
    new(elements_left)
  end
end
{:module, Stack, <<70, 79, 82, 49, 0, 0, 21, ...>>, {:drop, 2}}

Day 1

https://adventofcode.com/2022/day/1

defmodule Day1 do
  @moduledoc """
  Solutions for Day 1
  """

  @doc """
  Map of every elf's list of calories keyed by the index in order of appearance
  in the given data input file
  """
  @spec calories() :: %{(elf_index :: pos_integer()) => elf_calories :: [pos_integer()]}
  def calories() do
    handleChunk = fn chunk ->
      chunk
      |> Enum.reverse()
      |> Enum.map(&amp;String.to_integer/1)
    end

    chunk_fun = fn element, acc ->
      if element == "" do
        # Emit chunk and reset accumulator
        {:cont, handleChunk.(acc), []}
      else
        {:cont, [element | acc]}
      end
    end

    after_fun = fn acc ->
      {:cont, handleChunk.(acc), []}
    end

    Utilities.read_data(1)
    |> Stream.chunk_while([], chunk_fun, after_fun)
    |> Stream.with_index()
    |> Enum.into(%{}, fn {value, key} -> {key, value} end)
  end

  @doc """
  Returns the maximum total calories for a single elf
  """
  @spec max_calories() :: pos_integer()
  def max_calories() do
    calories()
    |> Enum.max(fn {_, a}, {_, b} -> Enum.sum(a) >= Enum.sum(b) end)
  end

  @doc """
  Returns a sorted list of tuples containing the elf's index and list of calories.
  The list is sorted by the total sum of each elf's calories.
  """
  @spec sorted_calories() :: [{elf_index :: pos_integer(), elf_calories :: [pos_integer()]}]
  def sorted_calories() do
    calories()
    |> Enum.sort(fn {_, a}, {_, b} -> Enum.sum(a) >= Enum.sum(b) end)
  end

  def part_one() do
    {_elf, calories} = max_calories()
    calories |> Enum.sum()
  end

  def part_two() do
    sorted_calories()
    |> Enum.take(3)
    |> Enum.map(fn {_k, v} -> Enum.sum(v) end)
    |> Enum.sum()
  end
end
{:module, Day1, <<70, 79, 82, 49, 0, 0, 18, ...>>, {:part_two, 0}}
Day1.calories()
%{
  33 => [1365, 4100, 3131, 3596, 4719, 4250, 4580, 5418, 1687, 6533, 5938, 5865, 4605],
  168 => [17215, 9922, 6402, 19625],
  117 => [14249, 5887, 12930, 2407, 14495],
  246 => [3583, 1792, 5592, 5648, 5637, 3586, 1685, 1042, 4020, 6204, 4111, 4887, 6858],
  175 => [4649, 7673, 4275, 3178, 5322, 4959, 5990, 1480, 5206, 3693],
  219 => [3959, 1907, 5456, 2086, 3711, 5971, 3294, 1861, 2780, 1913, 4832, 5076, 1830],
  12 => [8585, 9072, 3466],
  192 => [5832],
  188 => [5674, 1979, 7064, 2839, 6346, 2751, 1055, 3565, 6608, 2640, 2418],
  157 => [4261, 2686, 1891, 5516, 5035, 1525, 5360, 4027, 3811, 6325, 5813, 4172, 6415],
  132 => [4848, 2725, 1146, 5588, 1671, 4461, 1109, 2933, 1639, 4587, 6680, 1035, 3288],
  73 => [4927, 5047, 12168, 15288, 14202],
  44 => [1273, 2231, 3534, 4970, 1685, 2565, 1847, 5926, 5778, 4919, 4238, 1422, 3945, 1252, 4234],
  183 => [4998, 5114, 4812, 7291, 1189, 1140, 7418, 6180, 3614, 6185, 7174, 4112],
  124 => [7386, 9544, 7237],
  239 => [1071, 25798, 5915],
  170 => [14115, 1542, 17633],
  23 => [5148, 3451, 2735, 12950, 6576, 10785],
  29 => [2150, 2901, 3490, 4291, 8998, 5673, 3623, 3894, 7435],
  47 => [1501, 6913, 4990, 5948, 2597, 5644, 2876, 3515, 4264, 3768, 4731, 3944],
  89 => [8427, 7538, 7405, 6634, 2889, 1705, 7171, 6067, 3737],
  203 => [9432, 11708, 8636, 10825, 5787, 12812],
  61 => [2340, 9011, 8717, 1458, 11325, 4563, 8634],
  30 => [2235, 3164, 1231, 3675, 4188, 4865, 1611, 4652, 6333, 3262, 3124, 6215, 2704, 3183],
  43 => [3162, 4035, 1359, 4364, 2392, 3819, 5132, 4721, 4794, 1622, 4693, 5318, 6089, 1588, 3199],
  163 => [25171, 8046, 4857],
  39 => [2814, 5388, 1641, 1526, 4489, 2291, 2573, 5389, 3109, 3422, 5342, 1461, 3105, 4265],
  131 => [10460, 3439, 10344, 4353, 14564],
  45 => [9120, 13317, 3969, 4758, 13615, 7951],
  242 => [2689, 4455, 3461, 5468, 1308, 1111, 4181, 3832, 1172, 2809, 3782, 2943, 1926, 1470, 5324],
  235 => [3088, 5831, 5452, 2038, 1116, 3670, 3025, 3749, 5040, 2507, 3787, 1674, 4437, 2630, 5239],
  48 => [10132, 19382],
  145 => [1799, 3098, 4398, 3948, 4257, 4112, 1364, 3813, 5036, 3469, 4468, 2531, 1348],
  247 => [9068],
  171 => [3405, 3819, 1079, 2957, 2594, 3639, 1941, 3709],
  197 => [1192, 3957, 4091, 2855, 2872, 3079, 6075, 2858, 5498, 1143, 3565, 5869, 5297, 1698],
  57 => [7534, 2592, 1012, 4760, 2118, 8180, 1054, 3230, 7718],
  143 => [7627, 3776, 7000, 10332, 4325, 3876, 2789, 8333],
  237 => [2821, 7212, 7353, 5102, 3808, 7328, 2741, 1436, 6321, 6249, 6961],
  221 => [2776, 1481, 3775, 5827, 7069, 6834, 2274, 5570, 4355, 5544, ...],
  113 => [10645, 2358, 2163, 4477, 9860, 1345, 3691, 2085],
  225 => [11157, 5983, 7801, 8726, 1466, 4673, 4161],
  26 => [20192, 6831, 10425],
  69 => [9323, 7633, 11404, 5092, 1683, 2705, ...],
  88 => [2817, 26802],
  250 => [5676, 7085, 3867, 9663, ...],
  191 => [7035, 5732, 7114, ...],
  166 => [6177, 7217, ...],
  144 => [7701, ...],
  209 => [...],
  ...
}
Day1.sorted_calories()
[
  {34, [9739, 11547, 11940, 10268, 11939, 10825, 5522]},
  {224, [24591, 21630, 25260]},
  {75, [69228]},
  {121, [33797, 34907]},
  {148, [13509, 11975, 12287, 10636, 12399, 7668]},
  {158, [24072, 19037, 25145]},
  {46, [66512]},
  {81, [66023]},
  {6, [22590, 17677, 25444]},
  {151, [5490, 4786, 4274, 5379, 4549, 4178, 2801, 1439, 5323, 4588, 5163, 1764, 4159, 5876, 5784]},
  {36, [8045, 8298, 5813, 7881, 8605, 4114, 4524, 3837, 7631, 6763]},
  {156, [11372, 15360, 14226, 7483, 16378]},
  {2, [7069, 5792, 1519, 7380, 7034, 6203, 5706, 1850, 4933, 5562, 3826, 6661]},
  {24, [5578, 5978, 5716, 4026, 1429, 7684, 6552, 7630, 5834, 4936, 7936]},
  {0, [7769, 6798, 11685, 10826, 11807, 5786, 7932]},
  {78, [5366, 3942, 4203, 4337, 4559, 6474, 4586, 5663, 3658, 5700, 6654, 6103, 1057]},
  {83, [18357, 10466, 13614, 19749]},
  {20, [7772, 10595, 6827, 4469, 10385, 3420, 7657, 8950]},
  {31, [5215, 3259, 3550, 4965, 4096, 2036, 4517, 2554, 3266, 5571, 5266, 1466, 5259, 5882, 2782]},
  {183, [4998, 5114, 4812, 7291, 1189, 1140, 7418, 6180, 3614, 6185, 7174, 4112]},
  {203, [9432, 11708, 8636, 10825, 5787, 12812]},
  {180, [4784, 1321, 5487, 6182, 5706, 5460, 5418, 2886, 5906, 6230, 1211, 1325, 3766, 3473]},
  {58, [6017, 3402, 3896, 5801, 4055, 5034, 1063, 3694, 6663, 6079, 4364, 5712, 3366]},
  {65, [2592, 4604, 2710, 2273, 4703, 1705, 5528, 5053, 5556, 1562, 5820, 5347, 2978, 2734, 5782]},
  {186, [5484, 3947, 5030, 6716, 4840, 1353, 3667, 6815, 4884, 5050, 5678, 2224, 3010]},
  {90, [5708, 1287, 1923, 6351, 5986, 3291, 1295, 6439, 3591, 2498, 5135, 2927, 5476, 6504]},
  {201, [8320, 16041, 11934, 10727, 11090]},
  {133, [10223, 9558, 8984, 6259, 8858, 4175, 4696, 4865]},
  {209, [5634, 4583, 6213, 4733, 4093, 4552, 1115, 1413, 3362, 1079, 5891, 5299, 3532, 6085]},
  {15, [3035, 9421, 4497, 1802, 8447, 5675, 7580, 9053, 8004]},
  {237, [2821, 7212, 7353, 5102, 3808, 7328, 2741, 1436, 6321, 6249, 6961]},
  {98, [12541, 15894, 16431, 12428]},
  {17, [2982, 4005, 4036, 6510, 4817, 3958, 5057, 2049, 2603, 1227, 5960, 4043, 5261, 4780]},
  {71, [9990, 12357, 17458, 17411]},
  {162, [2745, 7978, 6573, 5295, 8034, 3894, 1410, 2764, 4644, 7553, 6257]},
  {172, [5027, 7097, 2180, 5866, 6339, 10088, 10331, 10009]},
  {221, [2776, 1481, 3775, 5827, 7069, 6834, 2274, 5570, 4355, 5544, 3954, ...]},
  {157, [4261, 2686, 1891, 5516, 5035, 1525, 5360, 4027, 3811, 6325, ...]},
  {194, [6075, 4309, 6002, 6426, 3744, 3310, 4451, 3076, 1509, ...]},
  {177, [8495, 4414, 7618, 7494, 4141, 10026, 7653, 6703]},
  {43, [3162, 4035, 1359, 4364, 2392, 3819, 5132, ...]},
  {38, [5882, 2097, 3154, 4166, 1556, 3414, ...]},
  {33, [1365, 4100, 3131, 3596, 4719, ...]},
  {125, [6602, 6957, 3141, 3585, ...]},
  {27, [3685, 8863, 1300, ...]},
  {59, [4197, 7022, ...]},
  {53, [7376, ...]},
  {147, [...]},
  {230, ...},
  {...},
  ...
]
Day1.part_one()
71780
Day1.part_two()
212489

Day 2

https://adventofcode.com/2022/day/2

defmodule Day2.PartOne do
  @moduledoc """
  Solution for Day 2 Part One
  """

  @typedoc """
  Represents a move in the game of rock, paper, scissors
  """
  @type move() :: :rock | :paper | :scissors

  @typedoc """
  Represents a single round of the game rock, paper, scissors
  """
  @type round() :: %{
          opponent: move(),
          response: move()
        }

  @typedoc """
  Represents the result of a single round of rock, paper, scissors
  """
  @type result() :: :win | :lose | :draw

  @doc """
  Parses a move consisting of "A", "B", "C", "X", "Y", "Z" into the corresponding
  move of `:rock`, `:paper`, or `:scissors`
  """
  @spec parse_move(String.t()) :: move()
  def parse_move(move) do
    case move do
      "A" -> :rock
      "B" -> :paper
      "C" -> :scissors
      "X" -> :rock
      "Y" -> :paper
      "Z" -> :scissors
    end
  end

  @doc """
  List of all rounds
  """
  @spec rounds() :: [round()]
  def rounds() do
    Utilities.read_data(2)
    |> Stream.map(fn <> <> " " <> response ->
      %{opponent: parse_move(opponent), response: parse_move(response)}
    end)
    |> Enum.to_list()
  end

  @doc """
  Judge the given round to determine if it is a win, loss, or draw for the player
  """
  @spec judge_round(round()) :: result()
  def judge_round(%{opponent: opponent, response: response}) do
    case {opponent, response} do
      {:rock, :rock} -> :draw
      {:rock, :paper} -> :win
      {:rock, :scissors} -> :lose
      {:paper, :rock} -> :lose
      {:paper, :paper} -> :draw
      {:paper, :scissors} -> :win
      {:scissors, :rock} -> :win
      {:scissors, :paper} -> :lose
      {:scissors, :scissors} -> :draw
    end
  end

  @doc """
  Score the round according to the given rubric that calculates a score based upon the
  reponse alone plus a score from the round's result
  """
  @spec score_round(round()) :: pos_integer()
  def score_round(%{opponent: _, response: response} = round) do
    response_score =
      case response do
        :rock -> 1
        :paper -> 2
        :scissors -> 3
      end

    outcome_score =
      case judge_round(round) do
        :win -> 6
        :lose -> 0
        :draw -> 3
      end

    response_score + outcome_score
  end

  @doc """
  A list of all the rounds' scores
  """
  @spec scored_rounds() :: [pos_integer()]
  def scored_rounds() do
    rounds()
    |> Enum.map(&amp;score_round/1)
  end

  def solution(), do: scored_rounds() |> Enum.sum()
end
{:module, Day2.PartOne, <<70, 79, 82, 49, 0, 0, 18, ...>>, {:solution, 0}}
Day2.PartOne.rounds()
[
  %{opponent: :paper, response: :paper},
  %{opponent: :rock, response: :scissors},
  %{opponent: :scissors, response: :scissors},
  %{opponent: :rock, response: :paper},
  %{opponent: :rock, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :scissors, response: :paper},
  %{opponent: :rock, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :rock, response: :paper},
  %{opponent: :paper, response: :scissors},
  %{opponent: :paper, response: :paper},
  %{opponent: :rock, response: :paper},
  %{opponent: :scissors, response: :paper},
  %{opponent: :paper, response: :rock},
  %{opponent: :paper, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :scissors, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :rock, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :rock, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :scissors, response: :paper},
  %{opponent: :rock, response: :paper},
  %{opponent: :paper, response: :rock},
  %{opponent: :paper, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :paper, response: :rock},
  %{opponent: :paper, response: :paper},
  %{opponent: :scissors, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :scissors, response: :scissors},
  %{opponent: :rock, response: :rock},
  %{opponent: :paper, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :rock, response: :scissors},
  %{opponent: :paper, response: :rock},
  %{opponent: :scissors, response: :paper},
  %{opponent: :scissors, response: :scissors},
  %{opponent: :paper, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :rock, response: :paper},
  %{opponent: :paper, response: :rock},
  %{opponent: :paper, response: :paper},
  %{opponent: :paper, response: :paper},
  %{opponent: :paper, ...},
  %{...},
  ...
]
Day2.PartOne.solution()
10404
defmodule Day2.PartTwo do
  @moduledoc """
  Solution for Day 2 Part Two
  """

  @typedoc """
  Represents a move in the game of rock, paper, scissors
  """
  @type move() :: :rock | :paper | :scissors

  @typedoc """
  Represents the result of a single round of rock, paper, scissors
  """
  @type result() :: :win | :lose | :draw

  @typedoc """
  Represents a single round of the game rock, paper, scissors
  """
  @type round() :: %{
          opponent: move(),
          response: move()
        }

  @typedoc """
  Represents a strategy for a single round of the game rock, paper, scissors
  """
  @type round_strategy() :: %{
          opponent: move(),
          expected_result: result()
        }

  @doc """
  Parses a move consisting of "A", "B", "C", "X", "Y", "Z" into the corresponding
  move of `:rock`, `:paper`, or `:scissors`
  """
  @spec parse_move(String.t()) :: move()
  def parse_move(move) do
    case move do
      "A" -> :rock
      "B" -> :paper
      "C" -> :scissors
    end
  end

  @doc """
  Parses an expected result consisting of "X", "Y", or "Z" into the corresponding
  result of `:win`, `:lose`, or `:draw`
  """
  @spec parse_expected_result(String.t()) :: result()
  def parse_expected_result(move) do
    case move do
      "X" -> :lose
      "Y" -> :draw
      "Z" -> :win
    end
  end

  @doc """
  List of all round stategies
  """
  @spec round_strategies() :: [round_strategy()]
  def round_strategies() do
    Utilities.read_data(2)
    |> Stream.map(fn <> <> " " <> expected_result ->
      %{
        opponent: parse_move(opponent),
        expected_result: parse_expected_result(expected_result)
      }
    end)
    |> Enum.to_list()
  end

  @doc """
  Judge the given round to determine if it is a win, loss, or draw for the player
  """
  @spec judge_round(round()) :: result()
  def judge_round(%{opponent: opponent, response: response}) do
    case {opponent, response} do
      {:rock, :rock} -> :draw
      {:rock, :paper} -> :win
      {:rock, :scissors} -> :lose
      {:paper, :rock} -> :lose
      {:paper, :paper} -> :draw
      {:paper, :scissors} -> :win
      {:scissors, :rock} -> :win
      {:scissors, :paper} -> :lose
      {:scissors, :scissors} -> :draw
    end
  end

  @doc """
  Score the round according to the given rubric that calculates a score based upon the
  reponse alone plus a score from the round's result
  """
  @spec score_round(round()) :: pos_integer()
  def score_round(%{opponent: _, response: response} = round) do
    response_score =
      case response do
        :rock -> 1
        :paper -> 2
        :scissors -> 3
      end

    outcome_score =
      case judge_round(round) do
        :win -> 6
        :lose -> 0
        :draw -> 3
      end

    response_score + outcome_score
  end

  @doc """
  Convert a strategy to a round by computing which move is required to respond to
  the opponent to guarantee the expected result
  """
  @spec convert_strategy_to_round(round_strategy()) :: round()
  def convert_strategy_to_round(round_strategy) do
    response =
      case {round_strategy.opponent, round_strategy.expected_result} do
        {:rock, :win} -> :paper
        {:rock, :lose} -> :scissors
        {:paper, :win} -> :scissors
        {:paper, :lose} -> :rock
        {:scissors, :win} -> :rock
        {:scissors, :lose} -> :paper
        {move, :draw} -> move
      end

    %{opponent: round_strategy.opponent, response: response}
  end

  @doc """
  A list of all the rounds' scores
  """
  @spec scored_rounds_with_strategy() :: [pos_integer()]
  def scored_rounds_with_strategy() do
    round_strategies()
    |> Enum.map(&amp;convert_strategy_to_round/1)
    |> Enum.map(&amp;score_round/1)
  end

  def solution(), do: scored_rounds_with_strategy() |> Enum.sum()
end
{:module, Day2.PartTwo, <<70, 79, 82, 49, 0, 0, 24, ...>>, {:solution, 0}}
Day2.PartTwo.round_strategies()
[
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :win, opponent: :rock},
  %{expected_result: :win, opponent: :scissors},
  %{expected_result: :draw, opponent: :rock},
  %{expected_result: :draw, opponent: :rock},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :scissors},
  %{expected_result: :draw, opponent: :rock},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :rock},
  %{expected_result: :win, opponent: :paper},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :rock},
  %{expected_result: :draw, opponent: :scissors},
  %{expected_result: :lose, opponent: :paper},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :scissors},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :rock},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :rock},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :scissors},
  %{expected_result: :draw, opponent: :rock},
  %{expected_result: :lose, opponent: :paper},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :lose, opponent: :paper},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :scissors},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :win, opponent: :scissors},
  %{expected_result: :lose, opponent: :rock},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :win, opponent: :rock},
  %{expected_result: :lose, opponent: :paper},
  %{expected_result: :draw, opponent: :scissors},
  %{expected_result: :win, opponent: :scissors},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :rock},
  %{expected_result: :lose, opponent: :paper},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :draw, opponent: :paper},
  %{expected_result: :lose, ...},
  %{...},
  ...
]
Day2.PartTwo.solution()
10334

Day 3

https://adventofcode.com/2022/day/3

defmodule Day3 do
  @moduledoc """
  Solutions for Day 3
  """

  @typedoc """
  Represents a single compartment
  """
  @type compartment() :: charlist()

  @typedoc """
  Represents a rucksack which consists of two equally sized compartments
  """
  @type rucksack() :: {
          first_compartment :: compartment(),
          second_compartment :: compartment()
        }

  @doc """
  Parse a rucksack string into a rucksack 2-tuple for the two
  compartments
  """
  @spec parse_rucksack(String.t()) :: rucksack()
  def parse_rucksack(data) do
    # Note that compartments are specified to be of the same length
    data
    |> String.to_charlist()
    |> Enum.split(div(String.length(data), 2))
  end

  @doc """
  A list of all the rucksacks
  """
  @spec rucksacks() :: [rucksack()]
  def rucksacks() do
    Utilities.read_data(3)
    |> Enum.map(&amp;parse_rucksack/1)
  end

  @doc """
  Given a charlist of a single element, calculate its priority according to the
  rubric of a -> z has priority 1 -> 26 and A -> Z has priority 27 -> 52
  """
  @spec assign_item_priority(charlist()) :: pos_integer()
  def assign_item_priority([char] = charlist) do
    # Note that all codepoints after 'a' and 'A' are sequential up to 'z'
    # and 'Z', respectively.
    if Utilities.lowercase?(to_string(charlist)) do
      # Assigns a -> z the priority 1 -> 26 by normalizing against the codepoint for 'a'
      char - ?a + 1
    else
      # Assigns A -> Z the priority 27 -> 52 by normalizing against the codepoint for 'A'
      char - ?A + 27
    end
  end

  def part_one() do
    rucksacks()
    # Convert each rucksack's compartments into sets and then find their
    # intersection and then assign a priority to that single element in
    # the intersection
    |> Enum.map(fn {first, second} ->
      MapSet.intersection(MapSet.new(first), MapSet.new(second))
      # Strip away the MapSet, leaving a charlist
      |> MapSet.to_list()
      |> assign_item_priority()
    end)
    # Sum up all the priorities, with one priority from each rucksack
    |> Enum.sum()
  end

  def part_two() do
    rucksacks()
    # Chunk every three rucksacks into a group
    |> Enum.chunk_every(3)
    # Convert all rucksacks in the group to sets and find their intersection
    # and then assign a priority to that single element in the intersection
    |> Enum.map(fn chunk ->
      chunk
      |> Enum.map(fn {first, second} ->
        (first ++ second)
        |> MapSet.new()
      end)
      |> Enum.reduce(&amp;MapSet.intersection/2)
      # Strip away the MapSet, leaving a charlist
      |> MapSet.to_list()
      |> assign_item_priority()
    end)
    # Sum up all the priorities, with one priority from each group
    |> Enum.sum()
  end
end
{:module, Day3, <<70, 79, 82, 49, 0, 0, 18, ...>>, {:part_two, 0}}
Day3.rucksacks()
[
  {'WwcsbsWw', 'spmFTGVV'},
  {'RHtMDHdSMnDBGMSDvnvDjt', 'mpTpjTFggpmjmTFggTjmpP'},
  {'vtCSGRMBDzHddvBHBzR', 'hrlcZhlLzWNlqblhzcr'},
  {'shhszHNHHZWqSzV', 'NdClMjlFjBBbNTB'},
  {'tQQGmnrMnJnGfmvrRR', 'PCjlbljFBdjFCjTjnP'},
  {'mRwtfGrMmJtwRDvQJQrJpM', 'LSzVDHzhzHZqZzqSzcWVWH'},
  {'WsWWgrtgsrhTQtsFcWPc', 'RMCCTvqvMvqNNqMMHlMq'},
  {'bBJrBGbzzLJznJrbSDGGJ', 'LqmlvqMqvlmLHRqRZZRNZ'},
  {'bzJfDGVSzVrJGwjVG', 'PPpQthdPsPpjdphsc'},
  {'pJpCCBSWlczWWBW', 'MHdMmMsFmpddrgF'},
  {'wfVqZZGVQv', 'zsMqmMgHjm'},
  {'vDZGvPttQTVtGDQDDDGw', 'bSCcSJSCJWTcRRSRczRJ'},
  {'HLVHsVWLwbWswbpWFWrrmT', 'hfTPNnhNSDDNhDfznTnhnS'},
  {'pBRcvGvvBtpGcqqQvgcp', 'hPfzfDGhzdzPDzDDhnhS'},
  {'ZQRvqBptjJgZCtJqq', 'MMMLHWwMWZWHHFFHm'},
  {'PvPFPvLLLSvNFvQNWNPvrPLr', 'ZjwhMttTwtTtQZBwqjqtZqwM'},
  {'HJDDbHjgppzCDCmzpgzsGbCs', 'TMZqZllqhJBhMTtVBBhMtMth'},
  {'zgGncmGGzHCnHDpDgDCGsm', 'FLLPFjPRRWLRjdcjrcdRLd'},
  {'zHnWzntnBRWTSBzRBdd', 'pFvZVcHpLFvjvLppvHP'},
  {'MmmWmNGQhbC', 'pZVLLbccvpj'},
  {'QDMCGrNWfwN', 'znBJsJzDBdg'},
  {'tcRcZccZmdZJctRcj', 'rlhNNDfrdNdSfNsNT'},
  {'QHQpBVvMpRMwgBgvnHR', 'FlhrSsgNFThgTFFflNS'},
  {'vvHpVBBBGBppHvpLvHG', 'bjmmtCqWLJJZRzZZZZb'},
  {'ZBtTDZRWsTsDZVWVZD', 'mjpbLbpSSzmLpWrbrS'},
  {'MFNNFvvwFHwlh', 'mNrCStLNtjzrb'},
  {'vwffwcHwflGqGflHJf', 'DBBZtQVBgZQJtBBsnT'},
  {'pTJcmMJTspmpMZZJJZ', 'HCQQMzPBlQdWWWFzWP'},
  {'LDnwrdnDnqjfqgvfDjrf', 'FlBBPFHFSHPQCBvQSSWB'},
  {'nLbjgLjdbrwV', 'RcppsscJVRRR'},
  {'mHnfggmM', 'tpHPPBCs'},
  {'PJjlQQRrJhJNPP', 'TtBsCbCCTlpptd'},
  {'rSSDhNQwShRRjh', 'mMPmzMDfPmfLzL'},
  {'HzLFBgrCthtFrrhFSCCCvB', 'QNRVmJJJmnpnddmppddVtJ'},
  {'MPZsjDWPjZs', 'VzNTzpVdRdZ'},
  {'qMfjWfwclsPsjwzq', 'HgLFhwGFwHrFFrSC'},
  {'llllmSbhNmSbNzlPmRN', 'CcgLLchHHpTGsCTQGpT'},
  {'dVjBrvBB', 'VLJQsLpC'},
  {'frZBWBDMFnd', 'StFsSwzlPlq'},
  {'vmTVVtmJHwCwDlltt', 'TsrcPcMrfqPMMpjMq'},
  {'LQGBRgGGRNgGgBhgzH', 'fpjPqsMjpLcLjrPLpq'},
  {'BdgzgSRGBnN', 'HJtJlVStVmt'},
  {'FbDQsFjPVHFZFSbrVjSVv', 'MJlGBJhDcqBBllJGccJnh'},
  {'RfTCTTpmppfgwCpwpLw', 'RMnMGMlcPGqhddPcJnl'},
  {'zgLPLNCCpLggzmTzTWm', 'VrjVvrNvjjjvbVHQZZH'},
  {'RBjjpwmRszB', 'dvhLdSvpVpV'},
  {'GrbfbJWmQJGWrGZZQMb', 'SLggfCgSHhCSgShghSC'},
  {'DWNDZQcrbWQrZJZGQQZ', 'PsztzBsPmBTzwcwRwjT'},
  {'rlvgglvZqbrbWbWWdvdm', ...},
  {...},
  ...
]
Day3.part_one()
7850
Day3.part_two()
2581

Day 4

https://adventofcode.com/2022/day/4

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(&amp;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(&amp;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
{:module, Day4, <<70, 79, 82, 49, 0, 0, 18, ...>>, {:part_two, 0}}
Day4.part_one()
462
Day4.part_two()
835

Day 5

https://adventofcode.com/2022/day/5

defmodule Day5 do
  @moduledoc """
  Solutions for Day 5
  """

  alias Stack

  @typedoc """
  Represents a crate's name or identifier as a string
  """
  @type crate() :: String.t()

  @typedoc """
  Represents a single stack of crates
  """
  @type stack() :: Stack.t(crate())

  @typedoc """
  Represents a stack's name
  """
  @type stack_name() :: non_neg_integer()

  @typedoc """
  Represents an instruction that says which crate to move from what stack
  to what stack
  """
  @type move_instruction() :: %{
          number_of_crates: non_neg_integer(),
          from: stack_name(),
          to: stack_name()
        }

  # @spec initial_stacks() :: [stack()]
  # def initial_stacks() do
  #   [
  #     ["F", "T", "N", "Z", "M", "G", "H", "J"],
  #     ["J", "W", "V"],
  #     ["H", "T", "B", "J", "L", "V", "G"],
  #     ["L", "V", "D", "C", "N", "J", "P", "B"],
  #     ["G", "R", "P", "M", "S", "W", "F"],
  #     ["M", "V", "N", "B", "F", "C", "H", "G"],
  #     ["R", "M", "G", "H", "D"],
  #     ["D", "Z", "V", "M", "N", "H"],
  #     ["H", "F", "N", "G"]
  #   ]
  #   |> Enum.map(&Stack.new/1)
  # end

  @doc """
  List of the initial stacks and their crates
  """
  @spec initial_stacks() :: [stack()]
  def initial_stacks() do
    Utilities.read_data(5, trim: false)
    # Get the stack data only, leaving off the instructions
    |> Enum.take_while(fn s -> s != "\n" end)
    # Chunk every line into "column" elements
    |> Enum.map(fn line ->
      line
      |> String.codepoints()
      |> Enum.chunk_every(4)
      |> Enum.map(&amp;Enum.join/1)
      |> Enum.map(&amp;String.trim/1)
    end)
    # Transpose the lines so as to turn each line into a single stack,
    # with the first element of the list being the top stack element
    |> Utilities.transpose()
    |> Enum.map(fn crates ->
      crates
      # Drop the stack number from the end
      |> Utilities.drop_last()
      # Get rid of the "empty" crates on top of the real crates
      |> Enum.filter(&amp;Utilities.non_empty?/1)
      # Parse the crates to just get the name
      |> Enum.map(fn "[" <> <> <> "]" -> crate end)
    end)
    |> Enum.map(&amp;Stack.new/1)
  end

  @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_move_instruction(String.t()) :: move_instruction()
  def parse_move_instruction(string) do
    ["move", number, "from", from, "to", to] = String.split(string, " ", trim: true)

    # The stack numbers 1-indexed in the problem but lists are 0-indexed in Elixir
    %{
      number_of_crates: String.to_integer(number),
      from: String.to_integer(from) - 1,
      to: String.to_integer(to) - 1
    }
  end

  @doc """
  List of all move instructions
  """
  @spec move_instructions() :: [move_instruction()]
  def move_instructions() do
    Utilities.read_data(5)
    |> Enum.drop_while(&amp;Utilities.non_empty?/1)
    |> Enum.map(&amp;String.trim/1)
    # This gets rid of any remaining empty lines
    |> Enum.filter(&amp;Utilities.non_empty?/1)
    |> Enum.map(&amp;parse_move_instruction/1)
  end

  @doc """
  Handle a move instruction for the case where crates are moved one at a time
  """
  @spec handle_move_instruction([stack()], move_instruction()) :: [stack()]
  def handle_move_instruction(stacks, move_instruction) do
    # Pop (really peek and drop) and push crates
    1..move_instruction.number_of_crates
    |> Enum.reduce(stacks, fn _, stacks ->
      # Get the crate that will be moved by peeking it from the "from" stack
      crate_to_move =
        stacks
        |> Enum.at(move_instruction.from)
        |> Stack.peek()

      # Drop the crate from the "from" stack and push the already peeked
      # crate to the "to" stack
      stacks
      |> List.update_at(move_instruction.from, &amp;Stack.drop/1)
      |> List.update_at(move_instruction.to, &amp;Stack.push(&amp;1, crate_to_move))
    end)
  end

  @doc """
  Handle a move instruction for the case where multiple crates are moved at once
  """
  @spec handle_move_instruction_in_order([stack()], move_instruction()) :: [stack()]
  def handle_move_instruction_in_order(stacks, move_instruction) do
    # Pop (really peek and drop) and push crates
    crates_to_move =
      stacks
      |> Enum.at(move_instruction.from)
      |> Stack.peek(move_instruction.number_of_crates)

    stacks
    |> List.update_at(
      move_instruction.from,
      &amp;Stack.drop(&amp;1, move_instruction.number_of_crates)
    )
    |> List.update_at(move_instruction.to, &amp;Stack.push_as_group(&amp;1, crates_to_move))
  end

  def part_one() do
    move_instructions()
    |> Enum.reduce(initial_stacks(), fn move_instruction, stacks ->
      handle_move_instruction(stacks, move_instruction)
    end)
    # Peek the top of all the final stacks
    |> Enum.map(&amp;Stack.peek/1)
    |> Enum.join()
  end

  def part_two() do
    move_instructions()
    |> Enum.reduce(initial_stacks(), fn move_instruction, stacks ->
      handle_move_instruction_in_order(stacks, move_instruction)
    end)
    # Peek the top of all the final stacks
    |> Enum.map(&amp;Stack.peek/1)
    |> Enum.join()
  end
end
{:module, Day5, <<70, 79, 82, 49, 0, 0, 31, ...>>, {:part_two, 0}}
Day5.initial_stacks()
[
  %Stack{elements: ["F", "T", "N", "Z", "M", "G", "H", "J"]},
  %Stack{elements: ["J", "W", "V"]},
  %Stack{elements: ["H", "T", "B", "J", "L", "V", "G"]},
  %Stack{elements: ["L", "V", "D", "C", "N", "J", "P", "B"]},
  %Stack{elements: ["G", "R", "P", "M", "S", "W", "F"]},
  %Stack{elements: ["M", "V", "N", "B", "F", "C", "H", "G"]},
  %Stack{elements: ["R", "M", "G", "H", "D"]},
  %Stack{elements: ["D", "Z", "V", "M", "N", "H"]},
  %Stack{elements: ["H", "F", "N", "G"]}
]
Day5.move_instructions()
[
  %{from: 3, number_of_crates: 6, to: 2},
  %{from: 7, number_of_crates: 5, to: 8},
  %{from: 3, number_of_crates: 1, to: 4},
  %{from: 3, number_of_crates: 1, to: 4},
  %{from: 1, number_of_crates: 2, to: 6},
  %{from: 0, number_of_crates: 2, to: 5},
  %{from: 5, number_of_crates: 9, to: 0},
  %{from: 2, number_of_crates: 12, to: 4},
  %{from: 7, number_of_crates: 1, to: 3},
  %{from: 0, number_of_crates: 3, to: 4},
  %{from: 5, number_of_crates: 1, to: 6},
  %{from: 4, number_of_crates: 10, to: 1},
  %{from: 4, number_of_crates: 14, to: 0},
  %{from: 6, number_of_crates: 8, to: 8},
  %{from: 1, number_of_crates: 11, to: 8},
  %{from: 2, number_of_crates: 1, to: 8},
  %{from: 0, number_of_crates: 11, to: 4},
  %{from: 0, number_of_crates: 2, to: 8},
  %{from: 3, number_of_crates: 1, to: 7},
  %{from: 0, number_of_crates: 6, to: 4},
  %{from: 7, number_of_crates: 1, to: 2},
  %{from: 4, number_of_crates: 16, to: 0},
  %{from: 0, number_of_crates: 4, to: 2},
  %{from: 4, number_of_crates: 1, to: 5},
  %{from: 2, number_of_crates: 4, to: 3},
  %{from: 5, number_of_crates: 1, to: 6},
  %{from: 8, number_of_crates: 21, to: 5},
  %{from: 0, number_of_crates: 2, to: 8},
  %{from: 3, number_of_crates: 2, to: 8},
  %{from: 8, number_of_crates: 5, to: 3},
  %{from: 0, number_of_crates: 9, to: 5},
  %{from: 3, number_of_crates: 6, to: 5},
  %{from: 5, number_of_crates: 1, to: 1},
  %{from: 6, number_of_crates: 1, to: 5},
  %{from: 2, number_of_crates: 1, to: 1},
  %{from: 5, number_of_crates: 8, to: 8},
  %{from: 0, number_of_crates: 3, to: 7},
  %{from: 1, number_of_crates: 1, to: 0},
  %{from: 5, number_of_crates: 13, to: 2},
  %{from: 0, number_of_crates: 1, to: 8},
  %{from: 0, number_of_crates: 2, to: 5},
  %{from: 7, number_of_crates: 3, to: 3},
  %{from: 3, number_of_crates: 4, to: 8},
  %{from: 0, number_of_crates: 3, to: 2},
  %{from: 8, number_of_crates: 22, to: 7},
  %{from: 1, number_of_crates: 1, to: 8},
  %{from: 7, number_of_crates: 6, to: 8},
  %{from: 5, number_of_crates: 15, ...},
  %{from: 7, ...},
  %{...},
  ...
]
Day5.handle_move_instruction(Day5.initial_stacks(), %{from: 4, number_of_crates: 6, to: 3})
[
  %Stack{elements: ["F", "T", "N", "Z", "M", "G", "H", "J"]},
  %Stack{elements: ["J", "W", "V"]},
  %Stack{elements: ["H", "T", "B", "J", "L", "V", "G"]},
  %Stack{elements: ["W", "S", "M", "P", "R", "G", "L", "V", "D", "C", "N", "J", "P", "B"]},
  %Stack{elements: ["F"]},
  %Stack{elements: ["M", "V", "N", "B", "F", "C", "H", "G"]},
  %Stack{elements: ["R", "M", "G", "H", "D"]},
  %Stack{elements: ["D", "Z", "V", "M", "N", "H"]},
  %Stack{elements: ["H", "F", "N", "G"]}
]
Day5.part_one()
"TDCHVHJTG"
Day5.part_two()
"NGCMPJLHV"

Day 6

https://adventofcode.com/2022/day/6

defmodule Day6 do
  @moduledoc """
  Solutions to Day 6
  """

  @doc """
  List of all the incoming data
  """
  @spec data_stream() :: [String.t()]
  def data_stream() do
    Utilities.read_data(6)
    |> Enum.to_list()
    |> hd()
    |> String.trim()
    |> String.codepoints()
  end

  @doc """
  Finds the index of the next element after the first window of the given size
  that contains unique elements
  """
  @spec find_end_of_first_distinct_window([any()], pos_integer()) :: pos_integer()
  def find_end_of_first_distinct_window(data_stream, window_size) do
    data_stream
    |> Enum.chunk_every(window_size, 1)
    |> Enum.find_index(&amp;Utilities.unique_elements?/1)
    |> Kernel.+(window_size)
  end

  def part_one() do
    data_stream()
    |> find_end_of_first_distinct_window(4)
  end

  def part_two() do
    data_stream()
    |> find_end_of_first_distinct_window(14)
  end
end
{:module, Day6, <<70, 79, 82, 49, 0, 0, 11, ...>>, {:part_two, 0}}
Day6.data_stream()
|> Enum.chunk_every(4, 1)
[
  ["b", "g", "d", "b"],
  ["g", "d", "b", "d"],
  ["d", "b", "d", "s"],
  ["b", "d", "s", "b"],
  ["d", "s", "b", "s"],
  ["s", "b", "s", "b"],
  ["b", "s", "b", "s"],
  ["s", "b", "s", "t"],
  ["b", "s", "t", "t"],
  ["s", "t", "t", "l"],
  ["t", "t", "l", "d"],
  ["t", "l", "d", "d"],
  ["l", "d", "d", "d"],
  ["d", "d", "d", "z"],
  ["d", "d", "z", "z"],
  ["d", "z", "z", "w"],
  ["z", "z", "w", "n"],
  ["z", "w", "n", "z"],
  ["w", "n", "z", "z"],
  ["n", "z", "z", "m"],
  ["z", "z", "m", "p"],
  ["z", "m", "p", "z"],
  ["m", "p", "z", "m"],
  ["p", "z", "m", "m"],
  ["z", "m", "m", "z"],
  ["m", "m", "z", "m"],
  ["m", "z", "m", "q"],
  ["z", "m", "q", "q"],
  ["m", "q", "q", "c"],
  ["q", "q", "c", "g"],
  ["q", "c", "g", "g"],
  ["c", "g", "g", "l"],
  ["g", "g", "l", "r"],
  ["g", "l", "r", "g"],
  ["l", "r", "g", "l"],
  ["r", "g", "l", "g"],
  ["g", "l", "g", "b"],
  ["l", "g", "b", "b"],
  ["g", "b", "b", "b"],
  ["b", "b", "b", "t"],
  ["b", "b", "t", "m"],
  ["b", "t", "m", "t"],
  ["t", "m", "t", "d"],
  ["m", "t", "d", "d"],
  ["t", "d", "d", "r"],
  ["d", "d", "r", "s"],
  ["d", "r", "s", ...],
  ["r", "s", ...],
  ["s", ...],
  [...],
  ...
]
Day6.part_one()
1794
Day6.part_two()
2851

Day 7

https://adventofcode.com/2022/day/7

defmodule Day7 do
  @moduledoc """
  Solutions to Day 7
  """

  @type structured_input() ::
          {:command, :change_directory, String.t()}
          | {:command, :list}
          | {:directory, name :: String.t()}
          | {:file, name :: String.t(), size :: integer()}

  @type directory_tree() ::
          {:directory, name :: String.t(), children :: directory_tree()}
          | {:file, name :: String.t(), size :: integer()}

  @spec parse_input(String.t()) :: structured_input()
  def parse_input("$ cd " <> directory) do
    {:command, :change_directory, directory}
  end

  def parse_input("$ ls") do
    {:command, :list}
  end

  def parse_input("dir " <> name) do
    {:directory, name}
  end

  def parse_input(file) do
    [size, name] = String.split(file, " ", trim: true)
    {:file, name, String.to_integer(size)}
  end

  @example_filesystem %{
    /: %{
      a: %{
        e: %{
          i: 584
        },
        f: 29116,
        g: 2557,
        "h.lst": 62596
      },
      "b.txt": 14_848_514,
      "c.dat": 8_504_156,
      d: %{
        j: 4_060_174,
        "d.log": 8_033_020,
        "d.ext": 5_626_152,
        k: 7_214_296
      }
    }
  }

  def calculate_directory_size(%{/: children} = filesystem) do
    Map.put(filesystem, :size, children |> Enum.map(&amp;calculate_directory_size/1) |> Enum.sum())
  end

  # def calculate_directory_size(%{filename => size}) when is_atom(filename) and is_integer(size) do
  #   size
  # end

  # def calculate_directory_size(%{directory => children})
  #     when is_atom(directory) and is_map(children) do
  #   %{
  #     directory => children,
  #     size: children |> Enum.map(&calculate_directory_size/1) |> Enum.sum()
  #   }
  # end

  @doc """
  List of all the incoming data
  """
  @spec inputs() :: [structured_input()]
  def inputs() do
    Utilities.read_data(7)
    |> Stream.map(&amp;parse_input/1)
    |> Enum.to_list()
  end

  def get_directory_access(base_directory, directory) do
    base_directory
    |> Path.join(directory)
    |> Path.split()
  end

  def filesystem_builder({:command, :change_directory, "/"}, {_current_directory, filesystem}) do
    {"/", filesystem}
  end

  def filesystem_builder({:command, :change_directory, ".."}, {current_directory, filesystem}) do
    {Path.dirname(current_directory), filesystem}
  end

  def filesystem_builder(
        {:command, :change_directory, directory},
        {current_directory, filesystem}
      ) do
    {Path.join(current_directory, directory), filesystem}
  end

  def filesystem_builder({:command, :list}, state) do
    state
  end

  def filesystem_builder({:directory, name}, {current_directory, filesystem}) do
    IO.inspect(current_directory, label: "current directory")
    IO.inspect(filesystem, label: "filesystem")
    {current_directory, put_in(filesystem, get_directory_access(current_directory, name), %{})}
  end

  def filesystem_builder({:file, name, size}, {current_directory, filesystem}) do
    new_filesystem =
      filesystem
      |> put_in(get_directory_access(current_directory, name), size)

    {current_directory, new_filesystem}
  end

  def build_filesystem() do
    inputs()
    |> List.foldl({nil, %{"/" => nil}}, &amp;filesystem_builder/2)
  end

  def part_one() do
    2
  end

  def part_two() do
    2
  end
end
warning: module attribute @example_filesystem was set but never used
  Documents/GitHub/advent-of-code/2022/elixir/advent_of_code_2022.livemd#cell:j6ujhu4gs6hshx7iax2bwa5q7btykmwe:34
{:module, Day7, <<70, 79, 82, 49, 0, 0, 22, ...>>, {:part_two, 0}}
Enum.map(%{a: 1, b: 2}, fn x -> x end)
[a: 1, b: 2]
Day7.inputs()
[
  {:command, :change_directory, "/"},
  {:command, :list},
  {:directory, "ccjp"},
  {:file, "hglnvs.bsh", 328708},
  {:directory, "hpsnpc"},
  {:directory, "pcb"},
  {:directory, "pntzm"},
  {:directory, "pzg"},
  {:directory, "thfgwwsp"},
  {:command, :change_directory, "ccjp"},
  {:command, :list},
  {:file, "dlz", 159990},
  {:directory, "mbtsvblj"},
  {:file, "nppbjl.qhg", 165076},
  {:command, :change_directory, "mbtsvblj"},
  {:command, :list},
  {:file, "frqsf.nsv", 34806},
  {:directory, "ppq"},
  {:directory, "ptht"},
  {:directory, "rgmvdwt"},
  {:command, :change_directory, "ppq"},
  {:command, :list},
  {:file, "dhzp", 266252},
  {:command, :change_directory, ".."},
  {:command, :change_directory, "ptht"},
  {:command, :list},
  {:directory, "jbnj"},
  {:directory, "zcbnwhzd"},
  {:command, :change_directory, "jbnj"},
  {:command, :list},
  {:directory, "clscr"},
  {:file, "zwtwf.zfz", 145780},
  {:command, :change_directory, "clscr"},
  {:command, :list},
  {:file, "dhzp", 249531},
  {:command, :change_directory, ".."},
  {:command, :change_directory, ".."},
  {:command, :change_directory, "zcbnwhzd"},
  {:command, :list},
  {:directory, "mbtsvblj"},
  {:command, :change_directory, "mbtsvblj"},
  {:command, :list},
  {:file, "pjhvzjt.brz", 258527},
  {:command, :change_directory, ".."},
  {:command, :change_directory, ".."},
  {:command, :change_directory, ".."},
  {:command, :change_directory, "rgmvdwt"},
  {:command, :list},
  {:file, ...},
  {...},
  ...
]
Day7.get_directory_access("/", "ccjp")
["/", "ccjp"]
put_in(%{}, ["ccjp"], %{"/" => nil})
%{"ccjp" => %{"/" => nil}}
Day7.build_filesystem()
current directory: "/"
filesystem: %{"/" => nil}
Day7.part_one()
2
Day7.part_two()
2

Tests

Writing tests for the solutions is important to re-verify solutions for changes that occur after a solution is first submitted and verified as correct. This ensures the solutions stay correct after refactors.

ExUnit.start(autorun: false)

defmodule AdventOfCode.Tests do
  use ExUnit.Case, async: true

  test "Day 1" do
    assert Day1.part_one() == 71_780
    assert Day1.part_two() == 212_489
  end

  test "Day 2" do
    assert Day2.PartOne.solution() == 10_404
    assert Day2.PartTwo.solution() == 10_334
  end

  test "Day 3" do
    assert Day3.part_one() == 7850
    assert Day3.part_two() == 2581
  end

  test "Day 4" do
    assert Day4.part_one() == 462
    assert Day4.part_two() == 835
  end

  test "Day 5" do
    assert Day5.part_one() == "TDCHVHJTG"
    assert Day5.part_two() == "NGCMPJLHV"
  end

  test "Day 6" do
    assert Day6.part_one() == 1794
    assert Day6.part_two() == 2851
  end

  test "Day 7" do
    assert Day7.part_one() == 1794
    assert Day7.part_two() == 2851
  end
end

ExUnit.run()
......

  1) test Day 7 (AdventOfCode.Tests)
     Documents/GitHub/advent-of-code/2022/elixir/advent_of_code_2022.livemd#cell:e5gr53niynkx7csnbjw53hy2qzhgy7uq:36
     Assertion with == failed
     code:  assert Day7.part_one() == 1794
     left:  2
     right: 1794
     stacktrace:
       Documents/GitHub/advent-of-code/2022/elixir/advent_of_code_2022.livemd#cell:e5gr53niynkx7csnbjw53hy2qzhgy7uq:37: (test)


Finished in 0.05 seconds (0.05s async, 0.00s sync)
7 tests, 1 failure

Randomized with seed 187784
%{excluded: 0, failures: 1, skipped: 0, total: 7}