Skip to content

Commit fd1f1c6

Browse files
authored
Add Access.slice/1 (#12008)
1 parent b859094 commit fd1f1c6

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed

lib/elixir/lib/access.ex

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,4 +829,118 @@ defmodule Access do
829829
defp get_and_update_filter([], _func, _next, updates, gets) do
830830
{:lists.reverse(gets), :lists.reverse(updates)}
831831
end
832+
833+
@doc ~S"""
834+
Returns a function that accesses all items of a list that are within the provided range.
835+
836+
The range will be normalized following the same rules from `Enum.slice/2`.
837+
838+
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
839+
`Kernel.get_and_update_in/3`, and friends.
840+
841+
## Examples
842+
843+
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]
844+
iex> get_in(list, [Access.slice(1..2), :name])
845+
["francine", "vitor"]
846+
iex> get_and_update_in(list, [Access.slice(1..3//2), :name], fn prev ->
847+
...> {prev, String.upcase(prev)}
848+
...> end)
849+
{["francine"], [%{name: "john", salary: 10}, %{name: "FRANCINE", salary: 30}, %{name: "vitor", salary: 25}]}
850+
851+
`slice/1` can also be used to pop elements out of a list or
852+
a key inside of a list:
853+
854+
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]
855+
iex> pop_in(list, [Access.slice(-2..-1)])
856+
{[%{name: "francine", salary: 30}, %{name: "vitor", salary: 25}], [%{name: "john", salary: 10}]}
857+
iex> pop_in(list, [Access.slice(-2..-1), :name])
858+
{["francine", "vitor"], [%{name: "john", salary: 10}, %{salary: 30}, %{salary: 25}]}
859+
860+
When no match is found, an empty list is returned and the update function is never called
861+
862+
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]
863+
iex> get_in(list, [Access.slice(5..10//2), :name])
864+
[]
865+
iex> get_and_update_in(list, [Access.slice(5..10//2), :name], fn prev ->
866+
...> {prev, String.upcase(prev)}
867+
...> end)
868+
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]}
869+
870+
An error is raised if the accessed structure is not a list:
871+
872+
iex> get_in(%{}, [Access.slice(2..10//3)])
873+
** (ArgumentError) Access.slice/1 expected a list, got: %{}
874+
875+
An error is raised if the step of the range is negative:
876+
877+
iex> get_in([], [Access.slice(2..10//-1)])
878+
** (ArgumentError) Access.slice/1 does not accept ranges with negative steps, got: 2..10//-1
879+
880+
"""
881+
@doc since: "1.14"
882+
@spec slice(Range.t()) :: access_fun(data :: list, current_value :: list)
883+
def slice(%Range{} = range) do
884+
if range.step > 0 do
885+
fn op, data, next -> slice(op, data, range, next) end
886+
else
887+
raise ArgumentError,
888+
"Access.slice/1 does not accept ranges with negative steps, got: #{inspect(range)}"
889+
end
890+
end
891+
892+
defp slice(:get, data, %Range{} = range, next) when is_list(data) do
893+
data
894+
|> Enum.slice(range)
895+
|> Enum.map(next)
896+
end
897+
898+
defp slice(:get_and_update, data, range, next) when is_list(data) do
899+
range = normalize_range(range, data)
900+
901+
if range.first > range.last do
902+
{[], data}
903+
else
904+
get_and_update_slice(data, range, next, [], [], 0)
905+
end
906+
end
907+
908+
defp slice(_op, data, _range, _next) do
909+
raise ArgumentError, "Access.slice/1 expected a list, got: #{inspect(data)}"
910+
end
911+
912+
defp normalize_range(%Range{first: first, last: last, step: step}, list)
913+
when first < 0 or last < 0 do
914+
count = length(list)
915+
first = if first >= 0, do: first, else: Kernel.max(first + count, 0)
916+
last = if last >= 0, do: last, else: last + count
917+
Range.new(first, last, step)
918+
end
919+
920+
defp normalize_range(range, _list), do: range
921+
922+
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
923+
if index in range do
924+
case next.(head) do
925+
:pop ->
926+
get_and_update_slice(rest, range, next, updates, [head | gets], index + 1)
927+
928+
{get, update} ->
929+
get_and_update_slice(
930+
rest,
931+
range,
932+
next,
933+
[update | updates],
934+
[get | gets],
935+
index + 1
936+
)
937+
end
938+
else
939+
get_and_update_slice(rest, range, next, [head | updates], gets, index + 1)
940+
end
941+
end
942+
943+
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
944+
{:lists.reverse(gets), :lists.reverse(updates)}
945+
end
832946
end

lib/elixir/test/elixir/access_test.exs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,86 @@ defmodule AccessTest do
135135
end
136136
end
137137

138+
describe "slice/1" do
139+
@test_list [1, 2, 3, 4, 5, 6, 7]
140+
141+
test "retrieves a range from the start of the list" do
142+
assert [2, 3] == get_in(@test_list, [Access.slice(1..2)])
143+
end
144+
145+
test "retrieves a range from the end of the list" do
146+
assert [6, 7] == get_in(@test_list, [Access.slice(-2..-1)])
147+
end
148+
149+
test "retrieves a range from positive first and negative last" do
150+
assert [2, 3, 4, 5, 6] == get_in(@test_list, [Access.slice(1..-2//1)])
151+
end
152+
153+
test "retrieves a range from negative first and positive last" do
154+
assert [6, 7] == get_in(@test_list, [Access.slice(-2..7//1)])
155+
end
156+
157+
test "retrieves a range with steps" do
158+
assert [1, 3] == get_in(@test_list, [Access.slice(0..2//2)])
159+
assert [2, 5] == get_in(@test_list, [Access.slice(1..4//3)])
160+
assert [2] == get_in(@test_list, [Access.slice(1..2//3)])
161+
assert [1, 3, 5, 7] == get_in(@test_list, [Access.slice(0..6//2)])
162+
end
163+
164+
test "pops a range from the start of the list" do
165+
assert {[2, 3], [1, 4, 5, 6, 7]} == pop_in(@test_list, [Access.slice(1..2)])
166+
end
167+
168+
test "pops a range from the end of the list" do
169+
assert {[6, 7], [1, 2, 3, 4, 5]} == pop_in(@test_list, [Access.slice(-2..-1)])
170+
end
171+
172+
test "pops a range from positive first and negative last" do
173+
assert {[2, 3, 4, 5, 6], [1, 7]} == pop_in(@test_list, [Access.slice(1..-2//1)])
174+
end
175+
176+
test "pops a range from negative first and positive last" do
177+
assert {[6, 7], [1, 2, 3, 4, 5]} == pop_in(@test_list, [Access.slice(-2..7//1)])
178+
end
179+
180+
test "pops a range with steps" do
181+
assert {[1, 3, 5], [2, 4, 6, 7]} == pop_in(@test_list, [Access.slice(0..4//2)])
182+
assert {[2], [1, 3, 4, 5, 6, 7]} == pop_in(@test_list, [Access.slice(1..2//2)])
183+
assert {[1, 4], [1, 2, 5, 6, 7]} == pop_in([1, 2, 1, 4, 5, 6, 7], [Access.slice(2..3)])
184+
end
185+
186+
test "updates range from the start of the list" do
187+
assert [-1, 2, 3, 4, 5, 6, 7] == update_in(@test_list, [Access.slice(0..0)], &(&1 * -1))
188+
189+
assert [1, -2, -3, 4, 5, 6, 7] == update_in(@test_list, [Access.slice(1..2)], &(&1 * -1))
190+
end
191+
192+
test "updates range from the end of the list" do
193+
assert [1, 2, 3, 4, 5, -6, -7] == update_in(@test_list, [Access.slice(-2..-1)], &(&1 * -1))
194+
195+
assert [-1, -2, 3, 4, 5, 6, 7] == update_in(@test_list, [Access.slice(-7..-6)], &(&1 * -1))
196+
end
197+
198+
test "updates a range from positive first and negative last" do
199+
assert [1, -2, -3, -4, -5, -6, 7] ==
200+
update_in(@test_list, [Access.slice(1..-2//1)], &(&1 * -1))
201+
end
202+
203+
test "updates a range from negative first and positive last" do
204+
assert [1, 2, 3, 4, 5, -6, -7] ==
205+
update_in(@test_list, [Access.slice(-2..7//1)], &(&1 * -1))
206+
end
207+
208+
test "updates a range with steps" do
209+
assert [-1, 2, -3, 4, -5, 6, 7] ==
210+
update_in(@test_list, [Access.slice(0..4//2)], &(&1 * -1))
211+
end
212+
213+
test "returns empty when the start of the range is greater than the end" do
214+
assert [] == get_in(@test_list, [Access.slice(2..1//1)])
215+
end
216+
end
217+
138218
describe "at/1" do
139219
@test_list [1, 2, 3, 4, 5, 6]
140220

0 commit comments

Comments
 (0)