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

Day7

2023/elixir/day7.livemd

Day7

Mix.install([
  {:kino_aoc, git: "https://github.com/ljgago/kino_aoc"},
  {:benchee, "~> 1.0"},
  {:nimble_parsec, "~> 1.0"},
  {:libgraph, "~> 0.16.0"}
])

Get Input

{:ok, data} = KinoAOC.download_puzzle("2023", "7", System.fetch_env!("LB_AOC_SECRET"))

Solve

defmodule Day7 do
  @cards ~w(A K Q J T 9 8 7 6 5 4 3 2)
  @cards2 ~w(A K Q T 9 8 7 6 5 4 3 2 J)

  @power_builder fn cards ->
    Enum.with_index(cards, fn el, ind -> {el, ind + 1} end) |> Map.new()
  end

  @power @power_builder.(@cards)
  @power2 @power_builder.(@cards2)

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

  def out(res, t), do: IO.puts("Res #{t}: #{res}")

  def run(data, part) do
    data
    |> String.split("\n", trim: true)
    |> parse()
    |> solve(part)
  end

  def parse(data) do
    data
    |> Enum.map(fn row ->
      [h, bid] = String.split(row)
      cards = String.graphemes(h)
      {build_hand(cards), cards, String.to_integer(bid)}
    end)
  end

  def build_hand(cards) do
    cards
    |> Enum.group_by(& &1)
    |> Enum.map(fn {k, v} -> {length(v), k} end)
    |> Enum.sort_by(fn {k, _v} -> k end, :desc)
  end

  def solve(data, part) do
    data
    |> Enum.sort_by(&card_sorter(&1, part), :desc)
    |> Enum.with_index(1)
    |> Enum.map(fn {{_h, _c, bid}, ind} -> bid * ind end)
    |> Enum.sum()
  end

  def card_sorter({hand, cards, _bid}, part) do
    tr = type_rank(hand, part)
    cr = cards_power(cards, part)
    {@type_rank[tr], cr}
  end

  def type_rank(hand, :p1) do
    hand
    |> Enum.map(fn {cnt, _card} -> cnt end)
    |> Enum.join()
  end

  def type_rank(hand, :p2) do
    numj =
      hand
      |> Enum.filter(fn {_cnt, c} -> c == "J" end)
      |> count_jokers()

    hand
    |> Enum.reject(fn {_cnt, c} -> c == "J" end)
    |> Enum.map(fn {cnt, _card} -> cnt end)
    |> add_jokers(numj)
    |> Enum.join()
  end

  def count_jokers([]), do: 0
  def count_jokers([{count, _j}]), do: count

  def add_jokers([], numj), do: [numj]
  def add_jokers([h | t], numj), do: [h + numj | t]

  def cards_power(cards, :p1), do: Enum.map(cards, fn c -> @power[c] end)
  def cards_power(cards, :p2), do: Enum.map(cards, fn c -> @power2[c] end)
end

Check

ExUnit.start(autorun: false)

defmodule AoCTest do
  use ExUnit.Case, async: false

  @td2 """
  77888 4
  77788 3
  T55J5 7
  KK677 2
  33332 6
  32T3K 1
  2AAAA 5
  KTJJT 9
  JJJJJ 10
  QQQJA 8
  """

  @td """
  32T3K 765
  T55J5 684
  KK677 28
  KTJJT 220
  QQQJA 483
  """

  setup do
    {:ok, data} = KinoAOC.download_puzzle("2023", "7", System.fetch_env!("LB_AOC_SECRET"))
    %{data: data}
  end

  test "solves test cases" do
    assert Day7.run(@td, :p1) == 6440
    assert Day7.run(@td, :p2) == 5905
  end

  test "solves edge cases" do
    assert Day7.run(@td2, :p1) == 333
    assert Day7.run(@td2, :p2) == 385
  end

  test "solves live cases", %{data: data} do
    assert Day7.run(data, :p1) == 253_638_586
    assert Day7.run(data, :p2) == 253_253_225
  end
end

ExUnit.run()

data |> Day7.run(:p1) |> Day7.out("part1")
data |> Day7.run(:p2) |> Day7.out("part2")