LoginSignup
3
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-20

とは?

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

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日)以上の間隔ごとに分割したい、要するにこうなって欲しい

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 で実現したい。

これとかこれとか読んでみたけど、どこにもそんなのは無かった。

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

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

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

つまり、こうなる。

%{
  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
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 を用いて比較する方法を教えて頂きました。
が、自分の無知を反省する意味でも、この記事は残しておきます。
知るべきコードはコメント欄をご覧ください。

3
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0