Skip to content

Commit c1d2bc7

Browse files
authored
Add Enum.slide/3 (#11349)
1 parent c5269a5 commit c1d2bc7

File tree

2 files changed

+298
-0
lines changed

2 files changed

+298
-0
lines changed

lib/elixir/lib/enum.ex

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2532,6 +2532,159 @@ defmodule Enum do
25322532
end
25332533
end
25342534

2535+
@doc """
2536+
Slides a single or multiple elements given by `range_or_single_index` from `enumerable`
2537+
to `insertion_index`.
2538+
2539+
The semantics of the range to be moved match the semantics of `Enum.slice/2`.
2540+
Specifically, that means:
2541+
2542+
* Indices are normalized, meaning that negative indexes will be counted from the end
2543+
(for example, -1 means the last element of the enumerable). This will result in *two*
2544+
traversals of your enumerable on types like lists that don't provide a constant-time count.
2545+
2546+
* If the normalized index range's `last` is out of bounds, the range is truncated to the last element.
2547+
2548+
* If the normalized index range's `first` is out of bounds, the selected range for sliding
2549+
will be empty, so you'll get back your input list.
2550+
2551+
* Decreasing ranges (such as `5..0//1`) also select an empty range to be moved,
2552+
so you'll get back your input list.
2553+
2554+
* Ranges with any step but 1 will raise an error.
2555+
2556+
## Examples
2557+
2558+
# Slide a single element
2559+
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 5, 1)
2560+
[:a, :f, :b, :c, :d, :e, :g]
2561+
2562+
# Slide a range of elements backward
2563+
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 3..5, 1)
2564+
[:a, :d, :e, :f, :b, :c, :g]
2565+
2566+
# Slide a range of elements forward
2567+
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 1..3, 5)
2568+
[:a, :e, :f, :b, :c, :d, :g]
2569+
2570+
# Slide with negative indices (counting from the end)
2571+
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 3..-1//1, 2)
2572+
[:a, :b, :d, :e, :f, :g, :c]
2573+
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], -4..-2, 1)
2574+
[:a, :d, :e, :f, :b, :c, :g]
2575+
2576+
"""
2577+
def slide(enumerable, range_or_single_index, insertion_index)
2578+
2579+
def slide(enumerable, single_index, insertion_index) when is_integer(single_index) do
2580+
slide(enumerable, single_index..single_index, insertion_index)
2581+
end
2582+
2583+
# This matches the behavior of Enum.slice/2
2584+
def slide(_, _.._//step = index_range, _insertion_index) when step != 1 do
2585+
raise ArgumentError,
2586+
"Enum.slide/3 does not accept ranges with custom steps, got: #{inspect(index_range)}"
2587+
end
2588+
2589+
# Normalize negative input ranges like Enum.slice/2
2590+
def slide(enumerable, first..last, insertion_index) when first < 0 or last < 0 do
2591+
count = Enum.count(enumerable)
2592+
normalized_first = if first >= 0, do: first, else: first + count
2593+
normalized_last = if last >= 0, do: last, else: last + count
2594+
2595+
if normalized_first >= 0 and normalized_first < count and normalized_first != insertion_index do
2596+
normalized_range = normalized_first..normalized_last//1
2597+
slide(enumerable, normalized_range, insertion_index)
2598+
else
2599+
Enum.to_list(enumerable)
2600+
end
2601+
end
2602+
2603+
def slide(enumerable, insertion_index.._, insertion_index) do
2604+
Enum.to_list(enumerable)
2605+
end
2606+
2607+
def slide(_, first..last, insertion_index)
2608+
when insertion_index > first and insertion_index < last do
2609+
raise "Insertion index for slide must be outside the range being moved " <>
2610+
"(tried to insert #{first}..#{last} at #{insertion_index})"
2611+
end
2612+
2613+
# Guarantees at this point: step size == 1 and first <= last and (insertion_index < first or insertion_index > last)
2614+
def slide(enumerable, first..last, insertion_index) do
2615+
impl = if is_list(enumerable), do: &slide_list_start/4, else: &slide_any/4
2616+
2617+
cond do
2618+
insertion_index <= first -> impl.(enumerable, insertion_index, first, last)
2619+
insertion_index > last -> impl.(enumerable, first, last + 1, insertion_index)
2620+
end
2621+
end
2622+
2623+
# Takes the range from middle..last and moves it to be in front of index start
2624+
defp slide_any(enumerable, start, middle, last) do
2625+
# We're going to deal with 4 "chunks" of the enumerable:
2626+
# 0. "Head," before the start index
2627+
# 1. "Slide back," between start (inclusive) and middle (exclusive)
2628+
# 2. "Slide front," between middle (inclusive) and last (inclusive)
2629+
# 3. "Tail," after last
2630+
#
2631+
# But, we're going to accumulate these into only two lists: pre and post.
2632+
# We'll reverse-accumulate the head into our pre list, then "slide back" into post,
2633+
# then "slide front" into pre, then "tail" into post.
2634+
#
2635+
# Then at the end, we're going to reassemble and reverse them, and end up with the
2636+
# chunks in the correct order.
2637+
{_size, pre, post} =
2638+
Enum.reduce(enumerable, {0, [], []}, fn item, {index, pre, post} ->
2639+
{pre, post} =
2640+
cond do
2641+
index < start -> {[item | pre], post}
2642+
index >= start and index < middle -> {pre, [item | post]}
2643+
index >= middle and index <= last -> {[item | pre], post}
2644+
true -> {pre, [item | post]}
2645+
end
2646+
2647+
{index + 1, pre, post}
2648+
end)
2649+
2650+
:lists.reverse(pre, :lists.reverse(post))
2651+
end
2652+
2653+
# Like slide_any/4 above, this optimized implementation of slide for lists depends
2654+
# on the indices being sorted such that we're moving middle..last to be in front of start.
2655+
defp slide_list_start([h | t], start, middle, last)
2656+
when start > 0 and start <= middle and middle <= last do
2657+
[h | slide_list_start(t, start - 1, middle - 1, last - 1)]
2658+
end
2659+
2660+
defp slide_list_start(list, 0, middle, last), do: slide_list_middle(list, middle, last, [])
2661+
2662+
defp slide_list_middle([h | t], middle, last, acc) when middle > 0 do
2663+
slide_list_middle(t, middle - 1, last - 1, [h | acc])
2664+
end
2665+
2666+
defp slide_list_middle(list, 0, last, start_to_middle) do
2667+
{slid_range, tail} = slide_list_last(list, last + 1, [])
2668+
slid_range ++ :lists.reverse(start_to_middle, tail)
2669+
end
2670+
2671+
# You asked for a middle index off the end of the list... you get what we've got
2672+
defp slide_list_middle([], _, _, acc) do
2673+
:lists.reverse(acc)
2674+
end
2675+
2676+
defp slide_list_last([h | t], last, acc) when last > 0 do
2677+
slide_list_last(t, last - 1, [h | acc])
2678+
end
2679+
2680+
defp slide_list_last(rest, 0, acc) do
2681+
{:lists.reverse(acc), rest}
2682+
end
2683+
2684+
defp slide_list_last([], _, acc) do
2685+
{:lists.reverse(acc), []}
2686+
end
2687+
25352688
@doc """
25362689
Applies the given function to each element in the `enumerable`,
25372690
storing the result in a list and passing it as the accumulator

lib/elixir/test/elixir/enum_test.exs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,151 @@ defmodule EnumTest do
803803
assert Enum.reverse_slice([1, 2, 3], 10, 10) == [1, 2, 3]
804804
end
805805

806+
describe "slide/3" do
807+
test "on an empty enum produces an empty list" do
808+
for enum <- [[], %{}, 0..-1//1, MapSet.new()] do
809+
assert Enum.slide(enum, 0..0, 0) == []
810+
end
811+
end
812+
813+
test "on a single-element enumerable is the same as transforming to list" do
814+
for enum <- [["foo"], [1], [%{foo: "bar"}], %{foo: :bar}, MapSet.new(["foo"]), 1..1] do
815+
assert Enum.slide(enum, 0..0, 0) == Enum.to_list(enum)
816+
end
817+
end
818+
819+
test "moves a single element" do
820+
for zero_to_20 <- [0..20, Enum.to_list(0..20)] do
821+
expected_numbers = Enum.flat_map([0..7, [14], 8..13, 15..20], &Enum.to_list/1)
822+
assert Enum.slide(zero_to_20, 14..14, 8) == expected_numbers
823+
end
824+
825+
assert Enum.slide([:a, :b, :c, :d, :e, :f], 3..3, 2) == [:a, :b, :d, :c, :e, :f]
826+
end
827+
828+
test "on a subsection of a list reorders the range correctly" do
829+
for zero_to_20 <- [0..20, Enum.to_list(0..20)] do
830+
expected_numbers = Enum.flat_map([0..7, 14..18, 8..13, 19..20], &Enum.to_list/1)
831+
assert Enum.slide(zero_to_20, 14..18, 8) == expected_numbers
832+
end
833+
834+
assert Enum.slide([:a, :b, :c, :d, :e, :f], 3..4, 2) == [:a, :b, :d, :e, :c, :f]
835+
end
836+
837+
test "handles negative indices" do
838+
make_negative_range = fn first..last, length ->
839+
(first - length)..(last - length)//1
840+
end
841+
842+
test_specs = [
843+
{[], 0..0, 0},
844+
{[1], 0..0, 0},
845+
{[-2, 1], 1..1, 1},
846+
{[4, -3, 2, -1], 3..3, 2},
847+
{[-5, -3, 4, 4, 5], 0..2, 3},
848+
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4..7, 9},
849+
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4..7, 0}
850+
]
851+
852+
for {list, range, insertion_point} <- test_specs do
853+
negative_range = make_negative_range.(range, length(list))
854+
855+
assert Enum.slide(list, negative_range, insertion_point) ==
856+
Enum.slide(list, range, insertion_point)
857+
end
858+
end
859+
860+
test "handles mixed positive and negative indices" do
861+
for zero_to_20 <- [0..20, Enum.to_list(0..20)] do
862+
assert Enum.slide(zero_to_20, -6..-1, 8) ==
863+
Enum.slide(zero_to_20, 15..20, 8)
864+
865+
assert Enum.slide(zero_to_20, 15..-1//1, 8) ==
866+
Enum.slide(zero_to_20, 15..20, 8)
867+
868+
assert Enum.slide(zero_to_20, -6..20, 8) ==
869+
Enum.slide(zero_to_20, 15..20, 8)
870+
end
871+
end
872+
873+
test "raises an error when the step is not exactly 1" do
874+
slide_ranges_that_should_fail = [2..10//2, 8..-1, 10..2//-1, 10..4//-2, -1..-8//-1]
875+
876+
for zero_to_20 <- [0..20, Enum.to_list(0..20)],
877+
range_that_should_fail <- slide_ranges_that_should_fail do
878+
assert_raise(ArgumentError, fn ->
879+
Enum.slide(zero_to_20, range_that_should_fail, 1)
880+
end)
881+
end
882+
end
883+
884+
test "doesn't change the order when the first and middle indices match" do
885+
for zero_to_20 <- [0..20, Enum.to_list(0..20)] do
886+
assert Enum.slide(zero_to_20, 8..18, 8) == Enum.to_list(0..20)
887+
end
888+
889+
assert Enum.slide([:a, :b, :c, :d, :e, :f], 1..3, 1) == [:a, :b, :c, :d, :e, :f]
890+
end
891+
892+
test "on the whole of an enumerable reorders it correctly" do
893+
for zero_to_20 <- [0..20, Enum.to_list(0..20)] do
894+
expected_numbers = Enum.flat_map([10..20, 0..9], &Enum.to_list/1)
895+
assert Enum.slide(zero_to_20, 10..20, 0) == expected_numbers
896+
end
897+
898+
assert Enum.slide([:a, :b, :c, :d, :e, :f], 4..5, 0) == [:e, :f, :a, :b, :c, :d]
899+
end
900+
901+
test "raises when the insertion point is inside the range" do
902+
for zero_to_20 <- [0..20, Enum.to_list(0..20)] do
903+
assert_raise RuntimeError, fn ->
904+
Enum.slide(zero_to_20, 10..18, 14)
905+
end
906+
end
907+
end
908+
909+
test "accepts range starts that are off the end of the enum, returning the input list" do
910+
assert Enum.slide([], 1..5, 0) == []
911+
912+
for zero_to_20 <- [0..20, Enum.to_list(0..20)] do
913+
assert Enum.slide(zero_to_20, 21..25, 3) == Enum.to_list(0..20)
914+
end
915+
end
916+
917+
test "accepts range ends that are off the end of the enum, truncating the moved range" do
918+
for zero_to_10 <- [0..10, Enum.to_list(0..10)] do
919+
assert Enum.slide(zero_to_10, 8..15, 4) == Enum.slide(zero_to_10, 8..10, 4)
920+
end
921+
end
922+
923+
test "matches behavior for lists vs. ranges" do
924+
range = 0..20
925+
list = Enum.to_list(range)
926+
# Below 32 elements, the map implementation currently sticks values in order.
927+
# If ever the MapSet implementation changes, this will fail (not affecting the correctness
928+
# of slide). I figured it'd be worth testing this for the time being just to have
929+
# another enumerable (aside from range) testing the generic implementation.
930+
set = MapSet.new(list)
931+
932+
test_specs = [
933+
{0..0, 0},
934+
{0..0, 20},
935+
{11..11, 14},
936+
{11..11, 3},
937+
{4..8, 19},
938+
{4..8, 0},
939+
{4..8, 2},
940+
{10..20, 0}
941+
]
942+
943+
for {slide_range, insertion_point} <- test_specs do
944+
slide = &Enum.slide(&1, slide_range, insertion_point)
945+
assert slide.(list) == slide.(set)
946+
assert slide.(list) == slide.(range)
947+
end
948+
end
949+
end
950+
806951
test "scan/2" do
807952
assert Enum.scan([1, 2, 3, 4, 5], &(&1 + &2)) == [1, 3, 6, 10, 15]
808953
assert Enum.scan([], &(&1 + &2)) == []

0 commit comments

Comments
 (0)