闘魂Elixir ── Advent of code 2023 Day 5 Part 1 を Livebook で楽しむ

<b><font color="red">$\huge{元氣ですかーーーーッ！！！}$</font></b>
<b><font color="red">$\huge{元氣があればなんでもできる！}$</font></b>

<b><font color="red">$\huge{闘魂とは己に打ち克つこと。}$</font></b>
<b><font color="red">$\huge{そして闘いを通じて己の魂を磨いていく}$</font></b>
<b><font color="red">$\huge{ことだと思います}$</font></b>

https://qiita.com/advent-calendar/2023/elixir

## はじめに

@torifukukaiou さんの ~~パク~~ リスペクト記事です

https://qiita.com/torifukukaiou/items/7dfd1439b1d6d82d5586

Elixir Livebook で Advent of Code 2023 の問題を解いてみます

https://adventofcode.com/2023

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/aoc/2023/2023_day5_part1.livemd

問題はこちら

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

## セットアップ

Kino AOC をインストールします

```elixir
Mix.install([
  {:kino_aoc, "~> 0.1.5"}
])
```

Kino AOC の使い方はこちらを参照

https://qiita.com/RyoWakabayashi/items/3d3abcf9134cee4dff05

## 入力の取得

Day 5 の入力を取得します

![スクリーンショット 2023-12-26 15.45.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/445befcf-8b89-bccb-be16-889b62c3f62d.png)

## 私の答え

私の答えです。
折りたたんでおきます。
▶を押して開いてください。

<details><summary>details</summary>

回答用のモジュールです

入力を扱いやすい形にするための `parse` と、回答を作るための `resolve` 関数を持っています

```elixir
defmodule Resolver do
  def parse(input) do
    input
    |> String.split("\n")
    |> split_by_blank_line([])
    |> Enum.into(%{}, fn group ->
      parse_group(group)
    end)
  end

  defp split_by_blank_line(lines, acc) do
    {head, tail} =
      Enum.split_while(lines, fn line -> line != "" end)

    if Enum.count(tail) == 0 do
      acc ++ [head]
    else
      tail
      |> tl()
      |> split_by_blank_line(acc ++ [head])
    end
  end

  defp parse_group(group) when length(group) == 1 do
    seeds =
      group
      |> hd()
      |> String.split(" ")
      |> tl()
      |> Enum.map(&String.to_integer(&1))

    {:seeds, seeds}
  end

  defp parse_group(group) do
    group_name =
      group
      |> hd()
      |> String.slice(0..-6)
      |> String.replace("-", "_")
      |> String.to_atom()

    mappings =
      group
      |> tl()
      |> Enum.map(fn line ->
        [dst, src, len] =
          line
          |> String.split(" ")
          |> Enum.map(&String.to_integer(&1))

        %{
          src: src,
          dst: dst,
          len: len
        }
      end)

    {group_name, mappings}
  end

  def resolve(maps) do
    maps.seeds
    |> Enum.map(fn seed ->
      seed
      |> search(maps.seed_to_soil)
      |> search(maps.soil_to_fertilizer)
      |> search(maps.fertilizer_to_water)
      |> search(maps.water_to_light)
      |> search(maps.light_to_temperature)
      |> search(maps.temperature_to_humidity)
      |> search(maps.humidity_to_location)
    end)
    |> Enum.min()
  end

  defp search(key, mappings) do
    target_mapping =
      Enum.find(mappings, fn mapping ->
        key >= mapping.src and key <= (mapping.src + mapping.len - 1)
      end)

    if is_nil(target_mapping) do
      key
    else
      target_mapping.dst + key - target_mapping.src
    end
  end
end
```

`parse` の時点でなかなか複雑です

入力の各行を空白行区切りでグループ化しています

`Enum.split_while` で空白行前後に分けるのを再帰する形にしましたが、余計に複雑になった気がします

`seeds` は種 ID の配列、それ以外は `%{src: <変換元開始インデックス>, dst: <変換先開始インデックス>, len: <変換範囲の長さ>}` の形式のマップの配列にしています

パースの例は以下のようになります

入力
```
seeds: 79 14 55 13

seed-to-soil map:
50 98 2
52 50 48

soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15

fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4

water-to-light map:
88 18 7
18 25 70

light-to-temperature map:
45 77 23
81 45 19
68 64 13

temperature-to-humidity map:
0 69 1
1 0 69

humidity-to-location map:
60 56 37
56 93 4
```

パース結果
```elixir
%{
  seeds: [79, 14, 55, 13],
  seed_to_soil: [%{len: 2, dst: 50, src: 98}, %{len: 48, dst: 52, src: 50}],
  soil_to_fertilizer: [
    %{len: 37, dst: 0, src: 15},
    %{len: 2, dst: 37, src: 52},
    %{len: 15, dst: 39, src: 0}
  ],
  fertilizer_to_water: [
    %{len: 8, dst: 49, src: 53},
    %{len: 42, dst: 0, src: 11},
    %{len: 7, dst: 42, src: 0},
    %{len: 4, dst: 57, src: 7}
  ],
  water_to_light: [%{len: 7, dst: 88, src: 18}, %{len: 70, dst: 18, src: 25}],
  light_to_temperature: [
    %{len: 23, dst: 45, src: 77},
    %{len: 19, dst: 81, src: 45},
    %{len: 13, dst: 68, src: 64}
  ],
  temperature_to_humidity: [%{len: 1, dst: 0, src: 69}, %{len: 69, dst: 1, src: 0}],
  humidity_to_location: [%{len: 37, dst: 60, src: 56}, %{len: 4, dst: 56, src: 93}]
}
```

`resolve` では用意した変換マップで各 ID を変換していきます

```elixir
  defp search(key, mappings) do
    target_mapping =
      Enum.find(mappings, fn mapping ->
        key >= mapping.src and key <= (mapping.src + mapping.len - 1)
      end)

    if is_nil(target_mapping) do
      key
    else
      target_mapping.dst + key - target_mapping.src
    end
  end
```

変換元インデックスが変換範囲内にあれば変換し、なければそのままの値を返します

ここまで用意した後、一気にパイプを繋げられるのが気持ちいいです

```elixir
      seed
      |> search(maps.seed_to_soil)
      |> search(maps.soil_to_fertilizer)
      |> search(maps.fertilizer_to_water)
      |> search(maps.water_to_light)
      |> search(maps.light_to_temperature)
      |> search(maps.temperature_to_humidity)
      |> search(maps.humidity_to_location)
```

</details>

## まとめ

問題を解く以上に入力をパースするのが大変でした

Part 2 はこちら

https://qiita.com/RyoWakabayashi/items/cccc5ba0c76a9e18abcd