とは?
例えば日付のリストがあったとして(前後関係は降順に並んでる前提)、
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日)以上の間隔ごとに分割したい、要するにこうなって欲しい
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 で実現したい。
これとかこれとか読んでみたけど、どこにもそんなのは無かった。
とりあえず動くことは動く
完成はしたが複雑怪奇になったので記録として残すことにした。
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 のリストには他言語と異なりインデックス(添字)という概念が無いので、
探索しやすいようインデックスをキーとするマップに変換する。
つまり、こうなる。
%{
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]
となっており、それぞれ
* 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) インデックスを探索できるようマップに変換
前後のインデックスを範囲として分割を行うために、インデックスでインデックスを(ややこしい)探索できるようにします。要するにこう。
%{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) で黒魔術を行っている。
まとめ
実行してみると期待通りの結果が得られる。
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
を用いて比較する方法を教えて頂きました。
が、自分の無知を反省する意味でも、この記事は残しておきます。
知るべきコードはコメント欄をご覧ください。