Skip to content

Latest commit

 

History

History
1289 lines (1055 loc) · 32.1 KB

sol.livemd

File metadata and controls

1289 lines (1055 loc) · 32.1 KB

AoC 2023

Mix.install([
  {:kino, "~> 0.11.3"}
])

Day 1

input =
  Kino.FS.file_path("day_1_input.txt")
  |> File.read!()
  |> String.split("\n")
defmodule Day1 do
  def run(input) do
    part_1 =
      input
      |> Enum.map(&leave_only_numbers/1)
      |> Enum.map(&format_numbers/1)
      |> Enum.sum()

    part_2 =
      input
      |> Enum.map(&written_number_to_number_string/1)
      |> Enum.map(&leave_only_numbers/1)
      |> Enum.map(&format_numbers/1)
      |> Enum.sum()

    {part_1, part_2}
  end

  defp written_number_to_number_string(string) do
    string
    |> String.replace("one", "o1e")
    |> String.replace("two", "t2o")
    |> String.replace("three", "t3e")
    |> String.replace("four", "f4r")
    |> String.replace("five", "f5e")
    |> String.replace("six", "s6x")
    |> String.replace("seven", "s7n")
    |> String.replace("eight", "e8t")
    |> String.replace("nine", "n9e")
  end

  defp leave_only_numbers(string) do
    String.replace(string, ~r/[^0-9]/, "")
  end

  defp format_numbers(""), do: 0

  defp format_numbers(string) do
    (String.at(string, 0) <> String.at(string, String.length(string) - 1))
    |> Integer.parse()
    |> elem(0)
  end
end
Day1.run(input)

Day 2

constrains = %{
  red: 12,
  green: 13,
  blue: 14
}

input =
  Kino.FS.file_path("day_2_input.txt")
  |> File.read!()
  |> String.split("\n")
defmodule Day2 do
  def run(input, constrains) do
    part_1 =
      input
      |> Enum.reduce(%{}, &get_game_info/2)
      |> Enum.filter(&is_game_possible?(&1, constrains))
      |> Enum.map(&elem(&1, 0))
      |> Enum.sum()

    part_2 =
      input
      |> Enum.reduce(%{}, &get_game_info/2)
      |> Enum.map(
        &(elem(&1, 1)
          |> calculate_fewest_to_make_possible()
          |> Map.values()
          |> Enum.product())
      )
      |> Enum.sum()

    {part_1, part_2}
  end

  defp calculate_fewest_to_make_possible(game_info) do
    Enum.reduce(game_info, %{red: 0, green: 0, blue: 0}, fn play, acc ->
      %{
        red: Enum.max([Map.get(play, :red) || 0, Map.get(acc, :red)]),
        green: Enum.max([Map.get(play, :green) || 0, Map.get(acc, :green)]),
        blue: Enum.max([Map.get(play, :blue) || 0, Map.get(acc, :blue)])
      }
    end)
  end

  defp is_game_possible?(game, constrains) do
    game
    |> elem(1)
    |> Enum.map(fn play ->
      (Map.get(play, :red) || 0) <= (Map.get(constrains, :red) || 0) &&
        (Map.get(play, :green) || 0) <= (Map.get(constrains, :green) || 0) &&
        (Map.get(play, :blue) || 0) <= (Map.get(constrains, :blue) || 0)
    end)
    |> Enum.all?()
  end

  # "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green"
  defp get_game_info(game_string, recursive_map) do
    [game_hd | game_tl] = String.split(game_string, ": ")
    game_tl = Enum.at(game_tl, 0)
    game_number = game_hd |> String.replace("Game ", "") |> Integer.parse() |> elem(0)
    game_info = game_tl |> String.split("; ") |> Enum.map(&get_play_info/1)

    Map.put(recursive_map, game_number, game_info)
  end

  # "3 blue, 4 red"
  defp get_play_info(play_string) do
    play_string
    |> String.split(", ")
    |> Enum.reduce(%{}, &accumulate_play_info/2)
  end

  # "3 blue, %{} -> %{blue: 3}
  defp accumulate_play_info(play, acc) do
    [number, color] = String.split(play, " ")
    Map.put(acc, String.to_atom(color), Integer.parse(number) |> elem(0))
  end
end
Day2.run(input, constrains)

Day 3

input =
  Kino.FS.file_path("day_3_input.txt")
  |> File.read!()
  |> String.split("\n")
  |> Enum.filter(&(&1 != ""))
defmodule Day3 do
  require Logger

  def run(input) do
    row_range = Enum.to_list(0..(length(input) - 1))

    part_1 =
      row_range
      |> Enum.reduce([], &generate_all_checks(input, &1, &2))
      |> Enum.filter(&filter_with_checks(input, &1))
      |> Enum.map(&Map.get(&1, :number))
      |> Enum.sum()

    part_2 =
      row_range
      |> Enum.reduce([], &generate_all_checks(input, &1, &2))
      |> Enum.reduce(%{}, &asterisk_transformation(input, &1, &2))
      |> Map.values()
      |> Enum.filter(&(length(&1) === 2))
      |> Enum.map(&Enum.product/1)
      |> Enum.sum()

    {part_1, part_2}
  end

  defp asterisk_transformation(input, check_map, accumulator) do
    checks = Map.get(check_map, :check)
    number = Map.get(check_map, :number)

    checks
    |> Enum.reduce(accumulator, fn {row, col}, acc ->
      line = Enum.at(input, row)
      char = String.at(line, col)

      with :error <- Integer.parse(char),
           true <- char === "*" do
        key = inspect({row, col})
        value = Map.get(acc, key, []) ++ [number]

        Map.put(acc, key, value)
      else
        _ -> acc
      end
    end)
  end

  defp filter_with_checks(input, check_map) do
    Map.get(check_map, :check)
    |> Enum.map(fn {row, col} ->
      line = Enum.at(input, row)
      char = String.at(line, col)

      with :error <- Integer.parse(char),
           true <- char !== "." do
        true
      else
        _ -> false
      end
    end)
    |> Enum.any?(& &1)
  end

  defp generate_all_checks(input, row_number, accumulator) do
    numbers_col_start_and_size = get_numbers_placement_per_row(input, row_number)

    line = Enum.at(input, row_number)

    row_checks =
      Enum.reduce(numbers_col_start_and_size, [], fn tuple, acc ->
        [col_start, size] = [elem(tuple, 0), elem(tuple, 1)]
        col_end = col_start + size
        max_row = length(input) - 1
        max_col = String.length(line) - 1

        [
          %{
            number: String.slice(line, col_start, size) |> String.to_integer(),
            check: generate_el_checks(row_number, col_start, col_end, max_row, max_col)
          }
          | acc
        ]
      end)

    row_checks ++ accumulator
  end

  defp generate_el_checks(row, col_start, col_end, max_row, max_col) do
    row_above = Enum.max([0, row - 1])
    row_below = Enum.min([max_row, row + 1])

    col_before = Enum.max([0, col_start - 1])
    col_after = Enum.min([max_col, col_end])

    range = Enum.to_list(col_before..col_after)

    [row_above, row, row_below]
    |> Enum.uniq_by(& &1)
    |> Enum.flat_map(fn row_number -> Enum.map(range, fn index -> {row_number, index} end) end)
  end

  defp get_numbers_placement_per_row(input, row) do
    Regex.scan(~r/\d+/, Enum.at(input, row), return: :index)
    |> Enum.map(&Enum.at(&1, 0))
  end
end
Day3.run(input)

Day 4

input =
  Kino.FS.file_path("day_4_input.txt")
  |> File.read!()
  |> String.split("\n")
  |> Enum.filter(&(&1 != ""))
require Logger

defmodule Day4 do
  def run(input) do
    part_1 =
      input
      |> Enum.map(fn card ->
        [hd, tl] = String.split(card, " | ")

        get_winning_numbers(hd)
        |> calculate_num_of_matches(get_my_numbers(tl))
        |> calculate_score()
      end)
      |> Enum.sum()

    quantities_per_card = input |> Enum.map(fn _ -> 1 end)

    part_2 =
      input
      |> Enum.with_index()
      |> Enum.reduce(quantities_per_card, fn {card, card_index}, acc ->
        [hd, tl] = String.split(card, " | ")
        num_of_matches = calculate_num_of_matches(get_winning_numbers(hd), get_my_numbers(tl))

        case num_of_matches do
          0 ->
            acc

          n ->
            curr_list = acc
            positions_to_increase = Enum.to_list((card_index + 1)..(card_index + n))
            amount_to_increase = Enum.at(acc, card_index)

            increase_numerical_list_at_positions(
              curr_list,
              positions_to_increase,
              amount_to_increase
            )
        end
      end)
      |> Enum.sum()

    {part_1, part_2}
  end

  defp get_winning_numbers(string) do
    string
    |> String.split(": ")
    |> Enum.at(1)
    |> String.split(" ")
    |> Enum.filter(&(&1 != ""))
    |> Enum.map(&String.to_integer/1)
  end

  defp get_my_numbers(string) do
    string
    |> String.split(" ")
    |> Enum.filter(&(&1 != ""))
    |> Enum.map(&String.to_integer/1)
  end

  defp calculate_num_of_matches(winning_numbers, my_numbers) do
    Enum.reduce(my_numbers, 0, fn my_num, acc ->
      case Enum.find(winning_numbers, &(&1 === my_num)) do
        nil ->
          acc

        _ ->
          acc + 1
      end
    end)
  end

  defp calculate_score(num_of_matches) do
    case num_of_matches do
      0 -> 0
      p -> 2 ** (p - 1)
    end
  end

  defp increase_numerical_list_at_positions(list, positions, increment) do
    Enum.with_index(list, fn el, idx ->
      case Enum.find(positions, &(&1 === idx)) do
        nil -> el
        _ -> el + increment
      end
    end)
  end
end
Day4.run(input)

Day 5

input =
  Kino.FS.file_path("day_5_input.txt")
  |> File.read!()
  |> String.split("\n")
defmodule Day5 do
  def run(input) do
    triples = get_triples(input)
    part_1 = exec(get_tuples_for_part_1(input), triples)
    part_2 = exec(get_tuples(input), triples)

    {part_1, part_2}
  end

  defp exec(tuples, triples) do
    triples
    |> Enum.reduce(tuples, fn triple, acc ->
      acc
      |> chop_tuples_from_triples(triple)
      |> apply_triples_in_tuples(triple)
    end)
    |> Enum.sort(fn {minA, _}, {minB, _} -> minA <= minB end)
    |> Enum.at(0)
    |> elem(0)
  end

  defp get_tuples_for_part_1(input) do
    input
    |> Enum.at(0)
    |> String.split(": ")
    |> Enum.at(1)
    |> String.split(" ")
    |> Enum.map(&String.to_integer/1)
    |> Enum.map(fn el -> {el, el} end)
  end

  defp get_tuples(input) do
    input
    |> Enum.at(0)
    |> String.split(": ")
    |> Enum.at(1)
    |> String.split(" ")
    |> Enum.map(&String.to_integer/1)
    |> Enum.chunk_every(2)
    |> Enum.map(fn chunk -> {Enum.at(chunk, 0), Enum.at(chunk, 0) + Enum.at(chunk, 1) - 1} end)
  end

  defp get_triples(input) do
    [_ | maps] =
      input
      |> Enum.chunk_by(&(&1 === ""))
      |> Enum.filter(fn arr -> Enum.all?(arr, fn el -> el !== "" end) end)

    maps
    |> Enum.map(fn [_ | tl] ->
      Enum.map(tl, fn el ->
        el
        |> String.split(" ")
        |> Enum.map(&String.to_integer/1)
      end)
      |> Enum.map(fn [target, source, length] ->
        {source, source + length - 1, target - source}
      end)
    end)
  end

  defp chop_tuple_from_triple(tuple, triple) do
    {triple_min, triple_max, _} = triple
    {tuple_min, tuple_max} = tuple

    cond do
      tuple_max < triple_min or tuple_min > triple_max ->
        [tuple]

      triple_min <= tuple_min and tuple_max <= triple_max ->
        [tuple]

      tuple_min < triple_min and tuple_max <= triple_max ->
        [
          {tuple_min, triple_min - 1},
          {triple_min, tuple_max}
        ]

      triple_min <= tuple_min and triple_max < tuple_max ->
        [
          {tuple_min, triple_max},
          {triple_max + 1, tuple_max}
        ]

      tuple_min < triple_min and triple_max < tuple_max ->
        [
          {tuple_min, triple_min - 1},
          {triple_min, triple_max},
          {triple_max + 1, tuple_max}
        ]
    end
  end

  defp chop_tuples_from_triple(tuples, triple) do
    Enum.flat_map(tuples, &chop_tuple_from_triple(&1, triple))
  end

  defp chop_tuple_from_triples(tuple, triples) do
    Enum.reduce(triples, [tuple], &chop_tuples_from_triple(&2, &1))
  end

  defp chop_tuples_from_triples(tuples, triples) do
    Enum.flat_map(tuples, &chop_tuple_from_triples(&1, triples))
  end

  defp apply_triple_in_tuple(tuple, triple) do
    {triple_min, triple_max, increment} = triple
    {tuple_min, tuple_max} = tuple

    cond do
      triple_min <= tuple_min and tuple_max <= triple_max ->
        new_tuple_min = tuple_min + increment
        new_tuple_max = tuple_max + increment
        {Enum.min([new_tuple_min, new_tuple_max]), Enum.max([new_tuple_min, new_tuple_max])}

      true ->
        tuple
    end
  end

  defp apply_triples_in_tuple(tuple, triples) do
    new_tuples = Enum.map(triples, &apply_triple_in_tuple(tuple, &1))
    contain_new? = Enum.any?(new_tuples, fn t -> t !== tuple end)

    case contain_new? do
      true -> Enum.filter(new_tuples, fn t -> t !== tuple end)
      false -> new_tuples |> Enum.uniq()
    end
  end

  defp apply_triples_in_tuples(tuples, triples) do
    Enum.flat_map(tuples, &apply_triples_in_tuple(&1, triples))
  end
end
Day5.run(input)

Day 6

input =
  Kino.FS.file_path("day_6_input.txt")
  |> File.read!()
  |> String.split("\n")
defmodule Day6 do
  def run(input) do
    part_1 = get_list_of_tuples_part_1(input) |> exec()
    part_2 = get_list_of_tuples_part_2(input) |> exec()

    {part_1, part_2}
  end

  defp exec(list_of_tuples) do
    list_of_tuples
    |> Enum.map(fn {max_time, distance_record} ->
      Enum.to_list(0..max_time)
      |> Enum.map(&calculate_distance(&1, max_time))
      |> Enum.count(&(&1 > distance_record))
    end)
    |> Enum.product()
  end

  defp get_list_of_tuples_part_1(input) do
    [times, distances] =
      input
      |> Enum.map(fn str ->
        str
        |> String.replace(~r/\s\s+/, " ")
        |> String.split(": ")
        |> Enum.at(1)
        |> String.split(" ")
        |> Enum.map(&String.to_integer/1)
      end)

    Enum.zip(times, distances)
  end

  defp get_list_of_tuples_part_2(input) do
    [times, distances] =
      input
      |> Enum.map(fn str ->
        str
        |> String.replace(~r/\s\s+/, " ")
        |> String.split(": ")
        |> Enum.at(1)
        |> String.replace(" ", "")
        |> String.to_integer()
      end)

    [{times, distances}]
  end

  defp calculate_distance(time_holding_button, max_time) do
    cond do
      time_holding_button === 0 -> 0
      true -> (max_time - time_holding_button) * time_holding_button
    end
  end
end
# Day6.run(input)

Day 7

input =
  Kino.FS.file_path("day_7_input.txt")
  |> File.read!()
  |> String.split("\n")
defmodule Cartesian do
  def product(characters, n) when is_integer(n) and n > 0 do
    Enum.chunk_every(List.flatten(generate_cartesian([], characters, n)), n)
  end

  defp generate_cartesian(acc, _characters, 0), do: [acc]

  defp generate_cartesian(acc, characters, n) do
    Enum.reduce(characters, [], fn char, result ->
      generate_cartesian([char | acc], characters, n - 1) ++ result
    end)
  end
end

defmodule CharReplacer do
  def exec(string, character, list_of_chars) do
    Enum.reduce(list_of_chars, string, fn letter, acc ->
      String.replace(acc, ~r/#{character}/, letter, global: false)
    end)
  end
end

defmodule Day7 do
  require Logger

  alias Cartesian

  @alphabet_1 %{
    "A" => 12,
    "K" => 11,
    "Q" => 10,
    "J" => 9,
    "T" => 8,
    "9" => 7,
    "8" => 6,
    "7" => 5,
    "6" => 4,
    "5" => 3,
    "4" => 2,
    "3" => 1,
    "2" => 0
  }

  @alphabet_2 %{
    "A" => 12,
    "K" => 11,
    "Q" => 10,
    "T" => 9,
    "9" => 8,
    "8" => 7,
    "7" => 6,
    "6" => 5,
    "5" => 4,
    "4" => 3,
    "3" => 2,
    "2" => 1,
    "J" => 0
  }

  @pc1 Cartesian.product(Map.keys(@alphabet_2) |> Enum.filter(&(&1 !== "J")), 1)

  @pc2 Cartesian.product(Map.keys(@alphabet_2) |> Enum.filter(&(&1 !== "J")), 2)

  @pc3 Cartesian.product(Map.keys(@alphabet_2) |> Enum.filter(&(&1 !== "J")), 3)

  @group_order %{
    "5" => 6,
    "41" => 5,
    "32" => 4,
    "311" => 3,
    "221" => 2,
    "2111" => 1,
    "11111" => 0
  }

  def run(input) do
    card_bid_list_map = get_card_bid_list_map(input)

    part_1 = exec(card_bid_list_map, &group_cards_1/1, @alphabet_1)
    part_2 = exec(card_bid_list_map, &group_cards_2/1, @alphabet_2)

    {part_1, part_2}
  end

  defp exec(card_bid_list_map, group_cards_fn, alphabet) do
    card_bid_list_map
    |> get_cards_from_card_bid_map_list()
    |> group_cards_fn.()
    |> sort_cards_on_each_group(alphabet)
    |> sort_groups()
    |> calculate(card_bid_list_map)
  end

  defp get_card_bid_list_map(input) do
    input
    |> Enum.map(fn s ->
      [card, bid] = String.split(s, " ")
      %{card => bid}
    end)
  end

  defp get_cards_from_card_bid_map_list(card_bid_list_map) do
    Enum.flat_map(card_bid_list_map, &Map.keys/1)
  end

  defp get_card_group(card) do
    card
    |> String.split("")
    |> Enum.filter(&(&1 !== ""))
    |> Enum.reduce(%{}, fn char, acc ->
      case Map.get(acc, char, nil) do
        nil -> Map.put(acc, char, 1)
        _ -> Map.put(acc, char, Map.get(acc, char) + 1)
      end
    end)
    |> Map.values()
    |> Enum.sort(fn a, b -> b < a end)
    |> Enum.map(&Integer.to_string/1)
    |> Enum.join()
  end

  defp group_cards_1(cards) do
    Enum.reduce(cards, %{}, fn card, acc ->
      group = get_card_group(card)

      case Map.get(acc, group, nil) do
        nil -> Map.put(acc, group, [card])
        _ -> Map.put(acc, group, [card | Map.get(acc, group)])
      end
    end)
  end

  defp get_card_group_2(card) do
    num_of_js =
      card
      |> String.split("")
      |> Enum.filter(&(&1 !== ""))
      |> Enum.count(fn s -> s === "J" end)

    case num_of_js do
      0 ->
        get_card_group(card)

      5 ->
        "5"

      4 ->
        "5"

      n ->
        [0, @pc1, @pc2, @pc3]
        |> Enum.at(n)
        |> Enum.map(fn letters ->
          get_card_group(CharReplacer.exec(card, "J", letters))
        end)
        |> Enum.sort(fn g_a, g_b ->
          Map.get(@group_order, g_a, 0) > Map.get(@group_order, g_b, 0)
        end)
        |> Enum.at(0)
    end
  end

  defp group_cards_2(cards) do
    Enum.reduce(cards, %{}, fn card, acc ->
      group = get_card_group_2(card)

      case Map.get(acc, group, nil) do
        nil -> Map.put(acc, group, [card])
        _ -> Map.put(acc, group, [card | Map.get(acc, group)])
      end
    end)
  end

  defp get_card_sorting_boolean(card_a, card_b, alphabet) do
    Enum.reduce_while(0..4, true, fn index, _ ->
      char_a = String.at(card_a, index)
      char_b = String.at(card_b, index)

      char_a_value = Map.get(alphabet, char_a, 0)
      char_b_value = Map.get(alphabet, char_b, 0)

      # Logger.info("Comparing: #{card_a}, #{char_a}, #{char_a_value} with #{card_b} #{char_b} #{char_b_value}")

      cond do
        char_a_value > char_b_value -> {:halt, false}
        char_a_value < char_b_value -> {:halt, true}
        true -> {:cont, true}
      end
    end)
  end

  defp get_group_sorting_boolean(group_a_label, group_b_label) do
    group_a_value = Map.get(@group_order, group_a_label, 0)
    group_b_value = Map.get(@group_order, group_b_label, 0)

    group_a_value < group_b_value
  end

  defp sort_cards_by_group(card_group, alphabet) do
    Enum.sort(card_group, &get_card_sorting_boolean(&1, &2, alphabet))
  end

  defp sort_cards_on_each_group(groups, alphabet) do
    Enum.map(groups, fn {label, group} -> %{label => sort_cards_by_group(group, alphabet)} end)
  end

  defp sort_groups(card_bid_map_sorted_by_cards) do
    Enum.sort(card_bid_map_sorted_by_cards, fn g_a, g_b ->
      group_a_label = Map.keys(g_a) |> Enum.at(0)
      group_b_label = Map.keys(g_b) |> Enum.at(0)
      get_group_sorting_boolean(group_a_label, group_b_label)
    end)
  end

  defp calculate(full_sorted, card_bid_list_map) do
    card_bid_map =
      Enum.reduce(card_bid_list_map, %{}, fn m, acc ->
        key = Map.keys(m) |> Enum.at(0)
        Map.put(acc, key, Map.get(m, key))
      end)

    full_sorted
    |> Enum.flat_map(fn m -> Map.values(m) end)
    |> Enum.flat_map(fn m -> m end)
    |> Enum.with_index()
    |> Enum.reduce(0, fn {card, index}, acc ->
      rank = index + 1
      bid = Map.get(card_bid_map, card, 0)
      acc + rank * String.to_integer(bid)
    end)
  end
end
Day7.run(input)

Day 8

input =
  Kino.FS.file_path("day_8_input.txt")
  |> File.read!()
  |> String.split("\n")
  |> Enum.filter(&(&1 !== ""))
defmodule BasicMath do
  def gcd(a, 0), do: a
  def gcd(0, b), do: b
  def gcd(a, b), do: gcd(b, rem(a, b))

  def lcm(0, 0), do: 0
  def lcm(a, b), do: trunc(a * b / gcd(a, b))
end

defmodule Day8 do
  require Logger

  alias BasicMath

  def run(input) do
    instructions = get_instructions(input)
    map = get_map(input)

    part_1 = navigate("AAA", "ZZZ", map, instructions)
    part_2 = get_origins(Map.keys(map)) |> get_cycle_size(map, instructions) |> lcm

    {part_1, part_2}
  end

  defp get_instructions(input) do
    [instructions | _] = input
    instructions
  end

  defp get_map(input) do
    [_ | raw_map] = input

    Enum.reduce(raw_map, %{}, fn entry, acc ->
      [key, lr] = String.split(entry, " = ")
      l = String.split(lr, ", ") |> Enum.at(0) |> String.replace("(", "")
      r = String.split(lr, ", ") |> Enum.at(1) |> String.replace(")", "")

      case Map.get(acc, key, nil) do
        nil -> Map.put(acc, key, {l, r})
        _ -> acc
      end
    end)
  end

  defp get_origins(origins) do
    Enum.filter(origins, &(String.at(&1, 2) === "A"))
  end

  defp navigate(origin, destiny, map, instructions, steps \\ 0) do
    instructions_list = String.split(instructions, "") |> Enum.filter(&(&1 !== ""))

    {updated_origin, updated_steps} =
      Enum.reduce(instructions_list, {origin, steps}, fn dir, acc ->
        idx = if dir === "L", do: 0, else: 1
        new_location = Map.get(map, elem(acc, 0)) |> elem(idx)

        {new_location, elem(acc, 1) + 1}
      end)

    case updated_origin do
      ^destiny -> updated_steps
      _ -> navigate(updated_origin, destiny, map, instructions, updated_steps)
    end
  end

  defp navigate_2(origin, map, instructions, steps \\ 0) do
    instructions_list = String.split(instructions, "") |> Enum.filter(&(&1 !== ""))

    {updated_origin, updated_steps} =
      Enum.reduce(instructions_list, {origin, steps}, fn dir, acc ->
        idx = if dir === "L", do: 0, else: 1
        new_location = Map.get(map, elem(acc, 0)) |> elem(idx)

        {new_location, elem(acc, 1) + 1}
      end)

    if String.at(updated_origin, 2) === "Z",
      do: updated_steps,
      else: navigate_2(updated_origin, map, instructions, updated_steps)
  end

  defp get_cycle_size(origins, map, instructions) do
    Enum.map(origins, fn origin -> navigate_2(origin, map, instructions) end)
  end

  defp lcm(list_of_numbers) do
    sorted_list_of_numbers = list_of_numbers |> Enum.sort(:asc)

    sorted_list_of_numbers
    |> Enum.reduce(Enum.at(sorted_list_of_numbers, 0), &BasicMath.lcm(&1, &2))
  end
end
Day8.run(input)

Day 9

input =
  Kino.FS.file_path("day_9_input.txt")
  |> File.read!()
  |> String.split("\n")
  |> Enum.filter(&(&1 !== ""))
defmodule Day9 do
  def run(input) do
    part_1 =
      input
      |> Enum.map(&convert_to_list_of_numbers/1)
      |> Enum.map(&convert_to_pyramid(&1, [&1]))
      |> Enum.map(&sum_pyramid_last_digits(&1))
      |> Enum.sum()

    part_2 =
      input
      |> Enum.map(&convert_to_list_of_numbers/1)
      |> Enum.map(&convert_to_pyramid(&1, [&1]))
      |> Enum.map(&smart_sum_pyramid_first_digits(&1))
      |> Enum.sum()

    {part_1, part_2}
  end

  defp convert_to_list_of_numbers(string) do
    String.split(string, " ") |> Enum.map(&String.to_integer/1)
  end

  defp convert_to_pyramid(list_of_numbers, acc \\ []) do
    list_of_differences =
      Enum.map(0..(length(list_of_numbers) - 2), fn idx ->
        Enum.at(list_of_numbers, idx + 1) - Enum.at(list_of_numbers, idx)
      end)

    case Enum.all?(list_of_differences, &(&1 === 0)) do
      true -> acc
      false -> convert_to_pyramid(list_of_differences, [list_of_differences] ++ acc)
    end
  end

  defp sum_pyramid_last_digits(pyramid) do
    pyramid
    |> Enum.map(&List.last/1)
    |> Enum.sum()
  end

  defp smart_sum_pyramid_first_digits(pyramid) do
    Enum.reduce(pyramid, 0, fn list_of_numbers, acc ->
      List.first(list_of_numbers) - acc
    end)
  end
end
Day9.run(input)

Day 10

input =
  Kino.FS.file_path("day_10_input.txt")
  |> File.read!()
  |> String.split("\n")
  |> Enum.filter(&(&1 !== ""))
  |> Enum.map(fn line -> String.split(line, "") |> Enum.filter(&(&1 !== "")) end)
defmodule Day10 do
  require Logger

  def run(input) do
    map = get_map(input)

    start_pos = get_start_pos(input)

    [dir_a_results, dir_b_results] =
      get_start_dirs(map, start_pos) |> Enum.map(&move(map, start_pos, &1))

    final =
      Map.keys(dir_b_results)
      |> Enum.map(fn key ->
        [dir_a_results, dir_b_results] |> Enum.map(&Map.get(&1, key)) |> Enum.min()
      end)

    part_1 = Enum.max(final)

    ###

    coords = get_coords(map, start_pos, Enum.at(get_start_dirs(map, start_pos), 0))

    part_2 = coords |> shoelace() |> pick(coords)

    {part_1, part_2}
  end

  defp get_map(input) do
    Enum.reduce(Enum.with_index(input), %{}, fn {row, row_idx}, acc1 ->
      Enum.reduce(Enum.with_index(row), acc1, fn {col, col_idx}, acc2 ->
        Map.put(acc2, inspect({row_idx, col_idx}), col)
      end)
    end)
  end

  defp get_start_pos(input) do
    row_idx = Enum.find_index(input, &Enum.member?(&1, "S"))
    col_idx = Enum.find_index(Enum.at(input, row_idx), &(&1 === "S"))

    {row_idx, col_idx}
  end

  defp get_start_dirs(map, {s_r, s_c}) do
    up = {s_r - 1, s_c}
    down = {s_r + 1, s_c}
    left = {s_r, s_c - 1}
    right = {s_r, s_c + 1}

    is_up? = Enum.member?(["|", "F", "7"], Map.get(map, tts(up)))
    is_down? = Enum.member?(["|", "J", "L"], Map.get(map, tts(down)))
    is_left? = Enum.member?(["-", "F", "L"], Map.get(map, tts(left)))
    is_right? = Enum.member?(["-", "J", "7"], Map.get(map, tts(right)))

    [
      {up, is_up?},
      {down, is_down?},
      {left, is_left?},
      {right, is_right?}
    ]
    |> Enum.filter(&elem(&1, 1))
    |> Enum.map(&elem(&1, 0))
  end

  defp get_coords(map, p, c, coords \\ []) do
    case Map.get(map, tts(c)) === "S" do
      true -> coords
      false -> get_coords(map, c, calculate(map, p, c), coords ++ [c])
    end
  end

  defp move(map, p, c, result_map \\ %{}) do
    case Map.get(map, tts(c)) === "S" do
      true ->
        result_map

      false ->
        value = Map.get(result_map, tts(p), 0) + 1
        result_map = Map.put(result_map, tts(c), value)
        move(map, c, calculate(map, p, c), result_map)
    end
  end

  defp shoelace(coords) do
    Enum.reduce(Enum.with_index(coords), 0, fn {coord, idx}, acc ->
      case idx === length(coords) - 1 do
        true -> acc + det(coord, Enum.at(coords, 0))
        false -> acc + det(coord, Enum.at(coords, idx + 1))
      end
    end)
  end

  defp pick(twoA, coords) do
    abs(twoA) / 2 + 1 - (length(coords) + 1) / 2
  end

  ###

  defp calculate(map, {p_x, p_y}, {c_x, c_y} = c) do
    pipe = Map.get(map, tts(c))

    case pipe do
      "." -> c
      "S" -> c
      "-" -> if p_y < c_y, do: {c_x, c_y + 1}, else: {c_x, c_y - 1}
      "|" -> if p_x < c_x, do: {c_x + 1, c_y}, else: {c_x - 1, c_y}
      "L" -> if p_x < c_x, do: {c_x, c_y + 1}, else: {c_x - 1, c_y}
      "F" -> if p_x === c_x, do: {c_x + 1, c_y}, else: {c_x, c_y + 1}
      "J" -> if p_y < c_y, do: {c_x - 1, c_y}, else: {c_x, c_y - 1}
      "7" -> if p_y < c_y, do: {c_x + 1, c_y}, else: {c_x, c_y - 1}
    end
  end

  # tuple to string
  defp tts({x, y}) do
    inspect({x, y})
  end

  # string to tuple
  defp stt(str) do
    [a, b] = String.split(str, ", ")

    {
      String.replace(a, "{", "") |> String.to_integer(),
      String.replace(b, "}", "") |> String.to_integer()
    }
  end

  def det({p_x, p_y}, {c_x, c_y}) do
    {x_1, y_1} = {p_y, p_x}
    {x_2, y_2} = {c_y, c_x}
    x_1 * y_2 - x_2 * y_1
  end
end
Day10.run(input)

Day 11

input =
  Kino.FS.file_path("day_11_input.txt")
  |> File.read!()
  |> String.split("\n")
  |> Enum.filter(&(&1 !== ""))
  |> Enum.map(fn row -> String.split(row, "") |> Enum.filter(&(&1 !== "")) end)
defmodule Day11 do
  def run(input) do
    sum_of_all_galaxy_pair_distances =
      input
      |> get_expanded_universe()
      |> get_universe_map()
      |> remove_empty_spaces()
      |> calculate_min_distances()
      |> Map.values()
      |> Enum.sum()

    part_1 = sum_of_all_galaxy_pair_distances / 2
  end

  defp get_universe_map(universe) do
    Enum.with_index(universe)
    |> Enum.reduce(%{}, fn {row, row_idx}, acc_1 ->
      Enum.reduce(Enum.with_index(row), acc_1, fn {col, col_idx}, acc_2 ->
        Map.put(acc_2, {row_idx, col_idx}, col)
      end)
    end)
  end

  defp get_universe_dimensions(universe) do
    {length(universe) - 1, length(Enum.at(universe, 0)) - 1}
  end

  defp get_expanded_universe(universe) do
    universe
    |> expand_universe_on_rows()
    |> expand_universe_on_cols()
  end

  defp expand_universe_on_rows(universe) do
    {max_rows, _} = get_universe_dimensions(universe)

    row_slices_idx =
      Enum.reduce(0..max_rows, [], fn row_idx, acc ->
        row = Enum.at(universe, row_idx)
        if Enum.all?(row, &(&1 === ".")), do: acc ++ [row_idx], else: acc
      end) ++ [max_rows]

    {_, expanded_result} =
      Enum.reduce(row_slices_idx, {0, []}, fn empty_row_idx, {last_row_idx, universe_slice} ->
        {empty_row_idx, universe_slice ++ Enum.slice(universe, last_row_idx..empty_row_idx)}
      end)

    expanded_result
  end

  defp expand_universe_on_cols(universe) do
    {_, max_cols} = get_universe_dimensions(universe)

    col_slices_idx =
      Enum.reduce(0..max_cols, [], fn col_idx, acc ->
        col = Enum.map(universe, &Enum.at(&1, col_idx))
        if Enum.all?(col, &(&1 === ".")), do: acc ++ [col_idx], else: acc
      end) ++ [max_cols]

    Enum.map(universe, fn row ->
      {_, col_expansion} =
        Enum.reduce(col_slices_idx, {0, []}, fn empty_col_idx, {last_col_idx, universe_slice} ->
          {empty_col_idx, universe_slice ++ Enum.slice(row, last_col_idx..empty_col_idx)}
        end)

      col_expansion
    end)
  end

  defp remove_empty_spaces(universe_map) do
    Enum.reduce(universe_map, %{}, fn {key, value}, acc ->
      if value !== ".", do: Map.put(acc, key, value), else: acc
    end)
  end

  defp calculate_min_distances(universe_map) do
    galaxies_coordinates = Map.keys(universe_map)

    Enum.reduce(universe_map, %{}, fn {key, _}, acc ->
      min_distance =
        Enum.map(galaxies_coordinates, fn {g_x, g_y} ->
          abs(g_x - elem(key, 0)) + abs(g_y - elem(key, 1))
        end)
        |> Enum.filter(&(&1 > 0))
        |> Enum.sum()

      Map.put(acc, key, min_distance)
    end)
  end
end
Day11.run(input)