Elixir でリストを前後の間隔ごとに分割する

## とは？

例えば日付のリストがあったとして(前後関係は降順に並んでる前提)、

```elixir:iex
iex(1)> dates
[
  ~D[2020-08-16], 
  ~D[2020-05-17], 
  ~D[2020-04-04], 
  ~D[2020-02-09], 
  ~D[2019-11-17],
  ~D[2019-07-21], 
  ~D[2019-05-12], 
  ~D[2019-03-30], 
  ~D[2018-12-15],
  ~D[2018-10-14], 
  ~D[2018-08-12], 
  ~D[2018-05-06]
]
```

これを前後3ヶ月(≒12週間=84日)以上の間隔ごとに分割したい、要するにこうなって欲しい

```elixir:iex
iex(2)> dates
[
  [~D[2020-08-16]],
  [~D[2020-05-17], ~D[2020-04-04], ~D[2020-02-09], ~D[2019-11-17]],
  [~D[2019-07-21], ~D[2019-05-12], ~D[2019-03-30]],
  [~D[2018-12-15], ~D[2018-10-14], ~D[2018-08-12]],
  [~D[2018-05-06]]
]
```

目で見たら簡単そうだけど、これを Elixir で実現したい。

[これ](https://hexdocs.pm/elixir/Enum.html)とか[これ](https://hexdocs.pm/elixir/List.html)とか読んでみたけど、どこにもそんなのは無かった。

## とりあえず動くことは動く

完成はしたが複雑怪奇になったので記録として残すことにした。

```elixir:chunk.ex
defmodule Chunk do
  def chunk_dates(list) do
    # 1) リストを探索できるようマップに変換
    map = Enum.with_index(list)
          |> Enum.map(fn({v, i}) -> {i, v} end)
          |> Map.new

    # 2) 84日以上の間隔が空いてる日付のインデックスを取得
    indexes = Enum.map(0..(length(list)-1-1), fn(i) ->
      if Date.diff(map[i], map[i+1]) > 84 do
        i
      end
    end)
    |> Enum.reject(&is_nil(&1))

    # 3) インデックスを(強引に)細工して次の処理の下準備
    # -1 を付け足す意味 => 後に +1 した時に 0 にするため
    # length(list)-1 を付け足す意味 => 最後の塊を取得するため
    indexes = [-1] ++ indexes ++ [length(list)-1]

    # 4) インデックスを探索できるようマップに変換
    index_map = Enum.with_index(indexes)
                |> Enum.map(fn({v, i}) -> {i, v} end)
                |> Map.new

    # 5) インデックスごとにスライス
    Enum.map(1..(length(indexes) - 1), fn(i) ->
      Enum.slice(list, (index_map[i-1]+1)..(index_map[i]))
    end)
  end
end
```

#### 1) リストを探索できるようマップに変換

Elixir のリストには他言語と異なり**インデックス(添字)**という概念が無いので、
探索しやすいよう**インデックスをキーとするマップ**に変換する。

つまり、こうなる。

```elixir
%{
  0 => ~D[2020-08-16],
  1 => ~D[2020-05-17],
  2 => ~D[2020-04-04],
  3 => ~D[2020-02-09],
  4 => ~D[2019-11-17],
  5 => ~D[2019-07-21],
  6 => ~D[2019-05-12],
  7 => ~D[2019-03-30],
  8 => ~D[2018-12-15],
  9 => ~D[2018-10-14],
  10 => ~D[2018-08-12],
  11 => ~D[2018-05-06]
}
```

#### 2) 84日以上の間隔が空いてる日付のインデックスを取得

`length(list)-1-1`は
「インデックスは0始まりなので-1」と「最後の日付は比較対象が存在しないので-1」
です。

この時の`indexes`は`[0, 4, 7, 10]`となっており、それぞれ

```elixir
* 0 => ~D[2020-08-16]
  1 => ~D[2020-05-17]
  2 => ~D[2020-04-04]
  3 => ~D[2020-02-09]
* 4 => ~D[2019-11-17]
  5 => ~D[2019-07-21]
  6 => ~D[2019-05-12]
* 7 => ~D[2019-03-30]
  8 => ~D[2018-12-15]
  9 => ~D[2018-10-14]
* 10 => ~D[2018-08-12]
  11 => ~D[2018-05-06]
```
を指している。次の日付から84日以上、間隔が空いていそう。

#### 3) インデックスを(強引に)細工して次の処理の下準備

最後に**インデックスをベースに前後で分割する**という処理を行うため、処理がしやすいよう細工します。

`indexes = [-1, 0, 4, 7, 10, 11]`

`-1`は`+1`した時に`indexes`のインデックス(ややこしい)の先頭(`0`)になるようにするため。
`11`は日付マップ(`map`)の最後のインデックス(次の比較先が無い)を分割の範囲に含めるため。

強引な黒魔術です。

#### 4) インデックスを探索できるようマップに変換

前後のインデックスを範囲として分割を行うために、インデックスでインデックスを(ややこしい)探索できるようにします。要するにこう。

```elixir
%{0 => -1, 1 => 0, 2 => 4, 3 => 7, 4 => 10, 5 => 11}
```

#### 5) インデックスごとにスライス

4) で制作したインデックスのそれぞれ前後を範囲として、対象リストを分割していく。
つまり`0〜0, 1〜4, 5〜7, 8〜10, 11〜11`を範囲として`Enum.slice`する。
このために 3) で黒魔術を行っている。

## まとめ

実行してみると期待通りの結果が得られる。

```elixir:iex
iex(3)> Chunk.chunk_dates(dates)
[
  [~D[2020-08-16]],
  [~D[2020-05-17], ~D[2020-04-04], ~D[2020-02-09], ~D[2019-11-17]],
  [~D[2019-07-21], ~D[2019-05-12], ~D[2019-03-30]],
  [~D[2018-12-15], ~D[2018-10-14], ~D[2018-08-12]],
  [~D[2018-05-06]]
]
```

と、こうして正常に動く処理はできたものの、あまりにも黒魔術が奇異すぎる。

もっと簡単なやり方、リーダブルな書き方をご存知な方がいましたら、
ぜひコメント欄にお願いします！

## 追記

### 2020-08-22

黒魔術でインデックスを使うより `Enum.at` を用いて比較する方法を教えて頂きました。
が、自分の無知を反省する意味でも、この記事は残しておきます。
知るべきコードはコメント欄をご覧ください。