今回の話は次のようなことを念頭に置いています。
これは順序を決定する情報を各要素が持っているばあい、たとえば次のようなばあい、
その情報を入れ替えることを意味します。
順序情報をもとに並び替えると次のようになります。
Elixir で、Elixir のデータ構造を使うケースと、Ecto (データベース)を使うケースを考えます。
タプルによる実装
まず次のようなタプル表現で考えます。
[{"Alice", 1}, {"Bob", 2}, {"Charlie", 3}, {"Dave", 4}, {"Erin", 5}]
前から後ろへ移動
前から後ろへ移動するばあい、次のような結果になればよいはずです。
[{"Alice", 1}, {"Bob", 4}, {"Charlie", 2}, {"Dave", 3}, {"Erin", 5}]
もしくは
[{"Alice", 1}, {"Charlie", 2}, {"Dave", 3}, {"Bob", 4}, {"Erin", 5}]
まず、Enum.split/2
を使って順序を入れ替える部分だけを切り出します。
users = [{"Alice", 1}, {"Bob", 2}, {"Charlie", 3}, {"Dave", 4}, {"Erin", 5}] #=> [{"Alice", 1}, {"Bob", 2}, {"Charlie", 3}, {"Dave", 4}, {"Erin", 5}] {leading, rest} = users |> Enum.split(1) #=> {[{"Alice", 1}], [{"Bob", 2}, {"Charlie", 3}, {"Dave", 4}, {"Erin", 5}]} {target, trailing} = rest |> Enum.split(3) #=> {[{"Bob", 2}, {"Charlie", 3}, {"Dave", 4}], [{"Erin", 5}]}
これで target
の範囲で順序情報を入れ替えればよいはずです。
次のコードでは Enum.chunk_every/3
を使って隣り合う要素の組みを作り、順序情報を隣に受け渡します。
末尾の要素の順序情報は、先頭の要素に受け渡します。
[{head_name, _} | _] = target target |> Enum.chunk_every(2, 1) |> Enum.map(fn [{_, order}, {name, _}] -> {name, order} [{_, order}] -> {head_name, order} end) #=> [{"Charlie", 2}, {"Dave", 3}, {"Bob", 4}]
この結果の前後に leading
と trailing
を連結すれば完了です。
後ろから前へ移動
後ろから前へ移動するばあい、分割までは同じですが、前から後ろへ移動する場合と逆の方向で順序情報を受け渡します。 先頭の要素の順序情報は、末尾の要素に受け渡します。
[{_, head_order} | _] = target target |> Enum.chunk_every(2, 1) |> Enum.map(fn [{name, _}, {_, order}] -> {name, order} [{name, _}] -> {name, head_order} end) #=> [{"Bob", 3}, {"Charlie", 4}, {"Dave", 2}]
補足 Enum.split/2
のふるまいについて
Enum.split/2
は、分割する位置が長さよりも大きいばあいには、常に全体と空リストに分割されます。
このため指定される位置が長さを超えていてもそのまま適用するできます。
[1, 2, 3, 4, 5] |> Enum.split(4) #=> {[1, 2, 3, 4], [5]} [1, 2, 3, 4, 5] |> Enum.split(5) #=> {[1, 2, 3, 4, 5], []} [1, 2, 3, 4, 5] |> Enum.split(6) #=> {[1, 2, 3, 4, 5], []} [1, 2, 3, 4, 5] |> Enum.split(7) #=> {[1, 2, 3, 4, 5], []}
負数のばあいは、末尾から分割されます。 位置が長さよりも大きいばあいと同じように、範囲外の場合に常に空リストと全体に分割されるようにするには、負数が与えられたときに 0 として扱うように調整する必要があります。
[1, 2, 3, 4, 5] |> Enum.split(1) #=> {[1], [2, 3, 4, 5]} [1, 2, 3, 4, 5] |> Enum.split(0) #=> {[], [1, 2, 3, 4, 5]} [1, 2, 3, 4, 5] |> Enum.split(-1) #=> {[1, 2, 3, 4], [5]} [1, 2, 3, 4, 5] |> Enum.split(-2) #=> {[1, 2, 3], [4, 5]}
Ecto による実装
次に Ecto で次のようなスキーマのデータを持つばあいを考えます。
defmodule User do use Ecto.Schema schema "users" do field :name, :string has_one :order, Order end end
defmodule Order do use Ecto.Schema schema "orders" do field :priority, :integer belongs_to :user, User end end
またデータベースには、すでに次のようにデータが格納されているとします。
from( user in User, join: order in Order, on: user.id == order.user_id, order_by: order.priority, select: {user.name, order.priority} ) |> MyApp.Repo.all #=> [{"Alice", 1}, {"Dave", 2}, {"Bob", 3}, {"Charlie", 4}, {"Erin", 5}]
前から後ろへ移動する
users = from( user in User, join: order in Order, on: user.id == order.user_id, order_by: order.priority ) |> preload(:order) |> MyApp.Repo.all() {_leading, rest} = users |> Enum.split(1) {target, _trailing} = rest |> Enum.split(3) [head | _] = target target |> Enum.map(& &1.order) |> Enum.chunk_every(2, 1) |> Enum.map(fn [%{priority: priority}, order] -> Ecto.Changeset.change(order, %{priority: priority}) [order] -> Ecto.Changeset.change(head.order, %{priority: order.priority}) end) |> Enum.map(&Repo.update/1)
本来は複数のレコードの更新を一体の操作にするためにトランザクション管理が必要ですが、今回は順序操作が主題なので横着しています。 詳しくはドキュメントを参照してください。
後ろから前へ移動する
users = from( user in User, join: order in Order, on: user.id == order.user_id, order_by: order.priority ) |> preload(:order) |> Repo.all() {_leading, rest} = users |> Enum.split(1) {target, _trailing} = rest |> Enum.split(3) [head | _] = target target |> Enum.map(& &1.order) |> Enum.chunk_every(2, 1) |> Enum.map(fn [order, %{priority: priority}] -> Ecto.Changeset.change(order, %{priority: priority}) [order] -> Ecto.Changeset.change(order, %{priority: head.order.priority}) end) |> Enum.map(&Repo.update/1)
必要な範囲のデータを取得する
上の例ではタプルで操作したときと同じように、すでに全体のデータを取得する前提で操作を組み立てました。
全体のデータは不要であれば、offset
と limit
を利用して最初から操作する範囲のみ取得して済ませることができます。
[head | _] = target = from( user in User, join: order in Order, on: user.id == order.user_id, order_by: order.priority, offset: 1, limit: 3 ) |> preload(:order) |> Repo.all()