今回の話は次のようなことを念頭に置いています。
1の要素を3の位置に移動する
これは順序を決定する情報を各要素が持っているばあい、たとえば次のようなばあい、
その情報を入れ替えることを意味します。
順序情報をもとに並び替えると次のようになります。
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 }]
{leading, rest} = users |> Enum . split(1 )
{target, trailing} = rest |> Enum . split(3 )
これで 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 )
この結果の前後に leading と trailing を連結すれば完了です。
後ろから前へ移動
後ろから前へ移動するばあい、分割までは同じですが、前から後ろへ移動する場合と逆の方向で順序情報を受け渡します。
先頭の要素の順序情報は、末尾の要素に受け渡します。
[{, head_order} | ] = target
target
|> Enum . chunk_every(2 , 1 )
|> Enum . map(fn
[{name, }, {, order}] -> {name, order}
[{name, }] -> {name, head_order}
end )
補足 Enum.split/2 のふるまいについて
Enum.split/2 は、分割する位置が長さよりも大きいばあいには、常に全体と空リストに分割されます。
このため指定される位置が長さを超えていてもそのまま適用するできます。
[1 , 2 , 3 , 4 , 5 ] |> Enum . split(4 )
[1 , 2 , 3 , 4 , 5 ] |> Enum . split(5 )
[1 , 2 , 3 , 4 , 5 ] |> Enum . split(6 )
[1 , 2 , 3 , 4 , 5 ] |> Enum . split(7 )
負数のばあいは、末尾から分割されます。
位置が長さよりも大きいばあいと同じように、範囲外の場合に常に空リストと全体に分割されるようにするには、負数が与えられたときに 0 として扱うように調整する必要があります。
[1 , 2 , 3 , 4 , 5 ] |> Enum . split(1 )
[1 , 2 , 3 , 4 , 5 ] |> Enum . split(0 )
[1 , 2 , 3 , 4 , 5 ] |> Enum . split(- 1 )
[1 , 2 , 3 , 4 , 5 ] |> Enum . split(- 2 )
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
前から後ろへ移動する
users =
from(
user in User ,
join: order in Order ,
on: user. id == order. user_id,
order_by: order. priority
)
|> preload(:order )
|> MyApp.Repo . all()
{, rest} = users |> Enum . split(1 )
{target, } = 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()
{, rest} = users |> Enum . split(1 )
{target, } = 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()