Help us understand the problem. What is going on with this article?

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

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

lassy
基本的には Ruby (on Rails) エンジニア。データベースやらネットワークやら、サーバサイド全般はそれなりに出来る(つもり)。最近は Elixir と Vue.js にハマってる(色んな意味で)ハマってます。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした