From c91a9eec81b4fc0526d988a6e23fa89338c6eb70 Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Wed, 27 Oct 2021 07:13:01 -0500 Subject: [PATCH 1/5] Add Enum.rotate/3 Previous discussion from the elixir-lang-core mailing list: https://groups.google.com/g/elixir-lang-core/c/LYmkUopaWN4/m/SwBzRt2zBQAJ --- lib/elixir/lib/enum.ex | 150 +++++++++++++++++++++++++++ lib/elixir/test/elixir/enum_test.exs | 142 +++++++++++++++++++++++++ 2 files changed, 292 insertions(+) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index a92af0ef02f..b2491b5021f 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -2532,6 +2532,156 @@ defmodule Enum do end end + @doc """ + Pulls out either a single element (denoted by an integer index) or a contiguous range of + the enumerable (given by a range) and inserts it in front of the value previously at the + insertion index. + + The semantics of the range to be rotated match the semantics of Enum.slice/2. Specifically, + that means: + + * Indices are normalized, meaning that negative indexes will be counted from the end + (for example, -1 means the last element of the enumerable). This will result in *two* + traversals of your enumerable on types like lists that don't provide a constant-time count. + * If the normalized index range's `last` is out of bounds, the range is truncated to the last element. + * If the normalized index range's `first` is out of bounds, the selected range for rotation + will be empty, so you'll get back your input list. + * Decreasing ranges (e.g., the range 5..0//1) also select an empty range to be rotated, so you'll + get back your input list. + * Ranges with any step but 1 will raise an error. + + ## Examples + + # Rotate a single element + iex> Enum.rotate([0, 1, 2, 3, 4, 5, 6], 5, 1) + [0, 5, 1, 2, 3, 4, 6] + + # Rotate a range of elements backward + iex> Enum.rotate([0, 1, 2, 3, 4, 5, 6], 3..5, 1) + [0, 3, 4, 5, 1, 2, 6] + + # Rotate a range of elements forward + iex> Enum.rotate([0, 1, 2, 3, 4, 5, 6], 1..3, 5) + [0, 4, 5, 1, 2, 3, 6] + + # Rotate with negative indices (counting from the end) + iex> Enum.rotate([0, 1, 2, 3, 4, 5, 6], 3..-1//1, 2) + [0, 1, 3, 4, 5, 6, 2] + iex> Enum.rotate([0, 1, 2, 3, 4, 5, 6], -4..-2, 1) + [0, 3, 4, 5, 1, 2, 6] + + """ + def rotate(enumerable, range_or_single_index, insertion_index) + + def rotate(enumerable, single_index, insertion_index) when is_integer(single_index) do + rotate(enumerable, single_index..single_index, insertion_index) + end + + # This matches the behavior of Enum.slice/2 + def rotate(_, _.._//step = index_range, _insertion_index) when step != 1 do + raise ArgumentError, + "Enum.rotate/3 does not accept ranges with custom steps, got: #{inspect(index_range)}" + end + + # Normalize negative input ranges like Enum.slice/2 + def rotate(enumerable, first..last, insertion_index) when first < 0 or last < 0 do + count = Enum.count(enumerable) + normalized_first = if first >= 0, do: first, else: first + count + normalized_last = if last >= 0, do: last, else: last + count + + if normalized_first >= 0 and normalized_first < count and normalized_first != insertion_index do + normalized_range = normalized_first..normalized_last//1 + rotate(enumerable, normalized_range, insertion_index) + else + Enum.to_list(enumerable) + end + end + + def rotate(enumerable, insertion_index.._, insertion_index) do + Enum.to_list(enumerable) + end + + def rotate(_, first..last, insertion_index) + when insertion_index > first and insertion_index < last do + raise "Insertion index for rotate must be outside the range being moved " <> + "(tried to insert #{first}..#{last} at #{insertion_index})" + end + + # Guarantees at this point: step size == 1 and first <= last and (insertion_index < first or insertion_index > last) + def rotate(enumerable, first..last, insertion_index) do + impl = if is_list(enumerable), do: &find_start/4, else: &rotate_any/4 + + cond do + insertion_index <= first -> impl.(enumerable, insertion_index, first, last) + insertion_index > last -> impl.(enumerable, first, last + 1, insertion_index) + end + end + + # Takes the range from middle..last and moves it to be in front of index start + defp rotate_any(enumerable, start, middle, last) do + # We're going to deal with 4 "chunks" of the enumerable: + # 0. "Head," before the start index + # 1. "Rotate back," between start (inclusive) and middle (exclusive) + # 2. "Rotate front," between middle (inclusive) and last (inclusive) + # 3. "Tail," after last + # + # But, we're going to accumulate these into only two lists: pre and post. + # We'll reverse-accumulate the head into our pre list, then "rotate back" into post, + # then "rotate front" into pre, then "tail" into post. + # + # Then at the end, we're going to reassemble and reverse them, and end up with the + # chunks in the correct order. + {_size, pre, post} = + Enum.reduce(enumerable, {0, [], []}, fn item, {index, pre, post} -> + {pre, post} = + cond do + index < start -> {[item | pre], post} + index >= start and index < middle -> {pre, [item | post]} + index >= middle and index <= last -> {[item | pre], post} + true -> {pre, [item | post]} + end + + {index + 1, pre, post} + end) + + :lists.reverse(pre, :lists.reverse(post)) + end + + # Like rotate_any/4 above, this optimized implementation of rotate for lists depends + # on the indices being sorted such that we're moving middle..last to be in front of start. + defp find_start([h | t], start, middle, last) + when start > 0 and start <= middle and middle <= last do + [h | find_start(t, start - 1, middle - 1, last - 1)] + end + + defp find_start(list, 0, middle, last), do: accumulate_start_middle(list, middle, last, []) + + defp accumulate_start_middle([h | t], middle, last, acc) when middle > 0 do + accumulate_start_middle(t, middle - 1, last - 1, [h | acc]) + end + + defp accumulate_start_middle(list, 0, last, start_to_middle) do + {rotated_range, tail} = accumulate_middle_last(list, last + 1, []) + rotated_range ++ :lists.reverse(start_to_middle, tail) + end + + # You asked for a middle index off the end of the list... you get what we've got + defp accumulate_start_middle([], _, _, acc) do + :lists.reverse(acc) + end + + defp accumulate_middle_last([h | t], last, acc) when last > 0 do + accumulate_middle_last(t, last - 1, [h | acc]) + end + + defp accumulate_middle_last(rest, 0, acc) do + {:lists.reverse(acc), rest} + end + + defp accumulate_middle_last([], _, acc) do + {:lists.reverse(acc), []} + end + @doc """ Applies the given function to each element in the `enumerable`, storing the result in a list and passing it as the accumulator diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index 29fd987aa7c..ba6a38badbf 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -803,6 +803,148 @@ defmodule EnumTest do assert Enum.reverse_slice([1, 2, 3], 10, 10) == [1, 2, 3] end + describe "rotate/3" do + test "on an empty enum produces an empty list" do + for enum <- [[], %{}, 0..-1//1, MapSet.new()] do + assert Enum.rotate(enum, 0..0, 0) == [] + end + end + + test "on a single-element enumerable is the same as transforming to list" do + for enum <- [["foo"], [1], [%{foo: "bar"}], %{foo: :bar}, MapSet.new(["foo"]), 1..1] do + assert Enum.rotate(enum, 0..0, 0) == Enum.to_list(enum) + end + end + + test "moves a single element" do + for zero_to_20 <- [0..20, Enum.to_list(0..20)] do + expected_numbers = Enum.flat_map([0..7, [14], 8..13, 15..20], &Enum.to_list/1) + assert Enum.rotate(zero_to_20, 14..14, 8) == expected_numbers + end + + assert Enum.rotate([:a, :b, :c, :d, :e, :f], 3..3, 2) == [:a, :b, :d, :c, :e, :f] + end + + test "on a subsection of a list reorders the range correctly" do + for zero_to_20 <- [0..20, Enum.to_list(0..20)] do + expected_numbers = Enum.flat_map([0..7, 14..18, 8..13, 19..20], &Enum.to_list/1) + assert Enum.rotate(zero_to_20, 14..18, 8) == expected_numbers + end + + assert Enum.rotate([:a, :b, :c, :d, :e, :f], 3..4, 2) == [:a, :b, :d, :e, :c, :f] + end + + test "handles negative indices" do + make_negative_range = fn first..last, length -> + (first - length)..(last - length)//1 + end + + test_specs = [ + {[], 0..0, 0}, + {[1], 0..0, 0}, + {[-2, 1], 1..1, 1}, + {[4, -3, 2, -1], 3..3, 2}, + {[-5, -3, 4, 4, 5], 0..2, 3}, + {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4..7, 9}, + {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4..7, 0} + ] + + for {list, range, insertion_point} <- test_specs do + negative_range = make_negative_range.(range, length(list)) + + assert Enum.rotate(list, negative_range, insertion_point) == + Enum.rotate(list, range, insertion_point) + end + end + + test "handles mixed positive and negative indices" do + for zero_to_20 <- [0..20, Enum.to_list(0..20)] do + assert Enum.rotate(zero_to_20, -6..-1, 8) == + Enum.rotate(zero_to_20, 15..20, 8) + + assert Enum.rotate(zero_to_20, 15..-1//1, 8) == + Enum.rotate(zero_to_20, 15..20, 8) + + assert Enum.rotate(zero_to_20, -6..20, 8) == + Enum.rotate(zero_to_20, 15..20, 8) + end + end + + test "raises an error when the step is not exactly 1" do + rotation_ranges_that_should_fail = [2..10//2, 8..-1, 10..2//-1, 10..4//-2, -1..-8//-1] + + for zero_to_20 <- [0..20, Enum.to_list(0..20)], + range_that_should_fail <- rotation_ranges_that_should_fail do + assert_raise(ArgumentError, fn -> + Enum.rotate(zero_to_20, range_that_should_fail, 1) + end) + end + end + + test "doesn't change the list when the first and middle indices match" do + for zero_to_20 <- [0..20, Enum.to_list(0..20)] do + assert Enum.rotate(zero_to_20, 8..18, 8) == Enum.to_list(0..20) + end + + assert Enum.rotate([:a, :b, :c, :d, :e, :f], 1..3, 1) == [:a, :b, :c, :d, :e, :f] + end + + test "on the whole of a list reorders it correctly" do + for zero_to_20 <- [0..20, Enum.to_list(0..20)] do + expected_numbers = Enum.flat_map([10..20, 0..9], &Enum.to_list/1) + assert Enum.rotate(zero_to_20, 10..20, 0) == expected_numbers + end + + assert Enum.rotate([:a, :b, :c, :d, :e, :f], 4..5, 0) == [:e, :f, :a, :b, :c, :d] + end + + test "raises when the insertion point is inside the range" do + for zero_to_20 <- [0..20, Enum.to_list(0..20)] do + assert_raise RuntimeError, fn -> + Enum.rotate(zero_to_20, 10..18, 14) + end + end + end + + test "accepts range starts that are off the end of the list, returning the input list" do + assert Enum.rotate([], 1..5, 0) == [] + + for zero_to_20 <- [0..20, Enum.to_list(0..20)] do + assert Enum.rotate(zero_to_20, 21..25, 3) == Enum.to_list(0..20) + end + end + + test "accepts range ends that are off the end of the list, truncating the rotated range" do + for zero_to_10 <- [0..10, Enum.to_list(0..10)] do + assert Enum.rotate(zero_to_10, 8..15, 4) == Enum.rotate(zero_to_10, 8..10, 4) + end + end + + test "matches behavior for lists vs. ranges" do + range = 0..20 + list = Enum.to_list(range) + # Below 32 elements, the map implementation currently sticks the pairs these in order + set = MapSet.new(list) + + test_specs = [ + {0..0, 0}, + {0..0, 20}, + {11..11, 14}, + {11..11, 3}, + {4..8, 19}, + {4..8, 0}, + {4..8, 2}, + {10..20, 0} + ] + + for {rotation_range, insertion_point} <- test_specs do + rotation = &Enum.rotate(&1, rotation_range, insertion_point) + assert rotation.(list) == rotation.(set) + assert rotation.(list) == rotation.(range) + end + end + end + test "scan/2" do assert Enum.scan([1, 2, 3, 4, 5], &(&1 + &2)) == [1, 3, 6, 10, 15] assert Enum.scan([], &(&1 + &2)) == [] From 7601125fa21df52fd8919caa5b489c094078c4af Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Fri, 29 Oct 2021 14:55:55 -0500 Subject: [PATCH 2/5] Rename Enum.rotate/3 to Enum.slide/3 per discussion on #11349 --- lib/elixir/lib/enum.ex | 64 +++++++++++++------------- lib/elixir/test/elixir/enum_test.exs | 68 ++++++++++++++-------------- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index b2491b5021f..7d3a819e31a 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -2537,79 +2537,79 @@ defmodule Enum do the enumerable (given by a range) and inserts it in front of the value previously at the insertion index. - The semantics of the range to be rotated match the semantics of Enum.slice/2. Specifically, + The semantics of the range to be moved match the semantics of Enum.slice/2. Specifically, that means: * Indices are normalized, meaning that negative indexes will be counted from the end (for example, -1 means the last element of the enumerable). This will result in *two* traversals of your enumerable on types like lists that don't provide a constant-time count. * If the normalized index range's `last` is out of bounds, the range is truncated to the last element. - * If the normalized index range's `first` is out of bounds, the selected range for rotation + * If the normalized index range's `first` is out of bounds, the selected range for sliding will be empty, so you'll get back your input list. - * Decreasing ranges (e.g., the range 5..0//1) also select an empty range to be rotated, so you'll + * Decreasing ranges (e.g., the range 5..0//1) also select an empty range to be moved, so you'll get back your input list. * Ranges with any step but 1 will raise an error. ## Examples - # Rotate a single element - iex> Enum.rotate([0, 1, 2, 3, 4, 5, 6], 5, 1) + # Slide a single element + iex> Enum.slide([0, 1, 2, 3, 4, 5, 6], 5, 1) [0, 5, 1, 2, 3, 4, 6] - # Rotate a range of elements backward - iex> Enum.rotate([0, 1, 2, 3, 4, 5, 6], 3..5, 1) + # Slide a range of elements backward + iex> Enum.slide([0, 1, 2, 3, 4, 5, 6], 3..5, 1) [0, 3, 4, 5, 1, 2, 6] - # Rotate a range of elements forward - iex> Enum.rotate([0, 1, 2, 3, 4, 5, 6], 1..3, 5) + # Slide a range of elements forward + iex> Enum.slide([0, 1, 2, 3, 4, 5, 6], 1..3, 5) [0, 4, 5, 1, 2, 3, 6] - # Rotate with negative indices (counting from the end) - iex> Enum.rotate([0, 1, 2, 3, 4, 5, 6], 3..-1//1, 2) + # Slide with negative indices (counting from the end) + iex> Enum.slide([0, 1, 2, 3, 4, 5, 6], 3..-1//1, 2) [0, 1, 3, 4, 5, 6, 2] - iex> Enum.rotate([0, 1, 2, 3, 4, 5, 6], -4..-2, 1) + iex> Enum.slide([0, 1, 2, 3, 4, 5, 6], -4..-2, 1) [0, 3, 4, 5, 1, 2, 6] """ - def rotate(enumerable, range_or_single_index, insertion_index) + def slide(enumerable, range_or_single_index, insertion_index) - def rotate(enumerable, single_index, insertion_index) when is_integer(single_index) do - rotate(enumerable, single_index..single_index, insertion_index) + def slide(enumerable, single_index, insertion_index) when is_integer(single_index) do + slide(enumerable, single_index..single_index, insertion_index) end # This matches the behavior of Enum.slice/2 - def rotate(_, _.._//step = index_range, _insertion_index) when step != 1 do + def slide(_, _.._//step = index_range, _insertion_index) when step != 1 do raise ArgumentError, - "Enum.rotate/3 does not accept ranges with custom steps, got: #{inspect(index_range)}" + "Enum.slide/3 does not accept ranges with custom steps, got: #{inspect(index_range)}" end # Normalize negative input ranges like Enum.slice/2 - def rotate(enumerable, first..last, insertion_index) when first < 0 or last < 0 do + def slide(enumerable, first..last, insertion_index) when first < 0 or last < 0 do count = Enum.count(enumerable) normalized_first = if first >= 0, do: first, else: first + count normalized_last = if last >= 0, do: last, else: last + count if normalized_first >= 0 and normalized_first < count and normalized_first != insertion_index do normalized_range = normalized_first..normalized_last//1 - rotate(enumerable, normalized_range, insertion_index) + slide(enumerable, normalized_range, insertion_index) else Enum.to_list(enumerable) end end - def rotate(enumerable, insertion_index.._, insertion_index) do + def slide(enumerable, insertion_index.._, insertion_index) do Enum.to_list(enumerable) end - def rotate(_, first..last, insertion_index) + def slide(_, first..last, insertion_index) when insertion_index > first and insertion_index < last do - raise "Insertion index for rotate must be outside the range being moved " <> + raise "Insertion index for slide must be outside the range being moved " <> "(tried to insert #{first}..#{last} at #{insertion_index})" end # Guarantees at this point: step size == 1 and first <= last and (insertion_index < first or insertion_index > last) - def rotate(enumerable, first..last, insertion_index) do - impl = if is_list(enumerable), do: &find_start/4, else: &rotate_any/4 + def slide(enumerable, first..last, insertion_index) do + impl = if is_list(enumerable), do: &find_start/4, else: &slide_any/4 cond do insertion_index <= first -> impl.(enumerable, insertion_index, first, last) @@ -2618,16 +2618,16 @@ defmodule Enum do end # Takes the range from middle..last and moves it to be in front of index start - defp rotate_any(enumerable, start, middle, last) do + defp slide_any(enumerable, start, middle, last) do # We're going to deal with 4 "chunks" of the enumerable: # 0. "Head," before the start index - # 1. "Rotate back," between start (inclusive) and middle (exclusive) - # 2. "Rotate front," between middle (inclusive) and last (inclusive) + # 1. "Slide back," between start (inclusive) and middle (exclusive) + # 2. "Slide front," between middle (inclusive) and last (inclusive) # 3. "Tail," after last # # But, we're going to accumulate these into only two lists: pre and post. - # We'll reverse-accumulate the head into our pre list, then "rotate back" into post, - # then "rotate front" into pre, then "tail" into post. + # We'll reverse-accumulate the head into our pre list, then "slide back" into post, + # then "slide front" into pre, then "tail" into post. # # Then at the end, we're going to reassemble and reverse them, and end up with the # chunks in the correct order. @@ -2647,7 +2647,7 @@ defmodule Enum do :lists.reverse(pre, :lists.reverse(post)) end - # Like rotate_any/4 above, this optimized implementation of rotate for lists depends + # Like slide_any/4 above, this optimized implementation of slide for lists depends # on the indices being sorted such that we're moving middle..last to be in front of start. defp find_start([h | t], start, middle, last) when start > 0 and start <= middle and middle <= last do @@ -2661,8 +2661,8 @@ defmodule Enum do end defp accumulate_start_middle(list, 0, last, start_to_middle) do - {rotated_range, tail} = accumulate_middle_last(list, last + 1, []) - rotated_range ++ :lists.reverse(start_to_middle, tail) + {slid_range, tail} = accumulate_middle_last(list, last + 1, []) + slid_range ++ :lists.reverse(start_to_middle, tail) end # You asked for a middle index off the end of the list... you get what we've got diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index ba6a38badbf..6e0054aa1c5 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -803,35 +803,35 @@ defmodule EnumTest do assert Enum.reverse_slice([1, 2, 3], 10, 10) == [1, 2, 3] end - describe "rotate/3" do + describe "slide/3" do test "on an empty enum produces an empty list" do for enum <- [[], %{}, 0..-1//1, MapSet.new()] do - assert Enum.rotate(enum, 0..0, 0) == [] + assert Enum.slide(enum, 0..0, 0) == [] end end test "on a single-element enumerable is the same as transforming to list" do for enum <- [["foo"], [1], [%{foo: "bar"}], %{foo: :bar}, MapSet.new(["foo"]), 1..1] do - assert Enum.rotate(enum, 0..0, 0) == Enum.to_list(enum) + assert Enum.slide(enum, 0..0, 0) == Enum.to_list(enum) end end test "moves a single element" do for zero_to_20 <- [0..20, Enum.to_list(0..20)] do expected_numbers = Enum.flat_map([0..7, [14], 8..13, 15..20], &Enum.to_list/1) - assert Enum.rotate(zero_to_20, 14..14, 8) == expected_numbers + assert Enum.slide(zero_to_20, 14..14, 8) == expected_numbers end - assert Enum.rotate([:a, :b, :c, :d, :e, :f], 3..3, 2) == [:a, :b, :d, :c, :e, :f] + assert Enum.slide([:a, :b, :c, :d, :e, :f], 3..3, 2) == [:a, :b, :d, :c, :e, :f] end test "on a subsection of a list reorders the range correctly" do for zero_to_20 <- [0..20, Enum.to_list(0..20)] do expected_numbers = Enum.flat_map([0..7, 14..18, 8..13, 19..20], &Enum.to_list/1) - assert Enum.rotate(zero_to_20, 14..18, 8) == expected_numbers + assert Enum.slide(zero_to_20, 14..18, 8) == expected_numbers end - assert Enum.rotate([:a, :b, :c, :d, :e, :f], 3..4, 2) == [:a, :b, :d, :e, :c, :f] + assert Enum.slide([:a, :b, :c, :d, :e, :f], 3..4, 2) == [:a, :b, :d, :e, :c, :f] end test "handles negative indices" do @@ -852,71 +852,71 @@ defmodule EnumTest do for {list, range, insertion_point} <- test_specs do negative_range = make_negative_range.(range, length(list)) - assert Enum.rotate(list, negative_range, insertion_point) == - Enum.rotate(list, range, insertion_point) + assert Enum.slide(list, negative_range, insertion_point) == + Enum.slide(list, range, insertion_point) end end test "handles mixed positive and negative indices" do for zero_to_20 <- [0..20, Enum.to_list(0..20)] do - assert Enum.rotate(zero_to_20, -6..-1, 8) == - Enum.rotate(zero_to_20, 15..20, 8) + assert Enum.slide(zero_to_20, -6..-1, 8) == + Enum.slide(zero_to_20, 15..20, 8) - assert Enum.rotate(zero_to_20, 15..-1//1, 8) == - Enum.rotate(zero_to_20, 15..20, 8) + assert Enum.slide(zero_to_20, 15..-1//1, 8) == + Enum.slide(zero_to_20, 15..20, 8) - assert Enum.rotate(zero_to_20, -6..20, 8) == - Enum.rotate(zero_to_20, 15..20, 8) + assert Enum.slide(zero_to_20, -6..20, 8) == + Enum.slide(zero_to_20, 15..20, 8) end end test "raises an error when the step is not exactly 1" do - rotation_ranges_that_should_fail = [2..10//2, 8..-1, 10..2//-1, 10..4//-2, -1..-8//-1] + slide_ranges_that_should_fail = [2..10//2, 8..-1, 10..2//-1, 10..4//-2, -1..-8//-1] for zero_to_20 <- [0..20, Enum.to_list(0..20)], - range_that_should_fail <- rotation_ranges_that_should_fail do + range_that_should_fail <- slide_ranges_that_should_fail do assert_raise(ArgumentError, fn -> - Enum.rotate(zero_to_20, range_that_should_fail, 1) + Enum.slide(zero_to_20, range_that_should_fail, 1) end) end end - test "doesn't change the list when the first and middle indices match" do + test "doesn't change the order when the first and middle indices match" do for zero_to_20 <- [0..20, Enum.to_list(0..20)] do - assert Enum.rotate(zero_to_20, 8..18, 8) == Enum.to_list(0..20) + assert Enum.slide(zero_to_20, 8..18, 8) == Enum.to_list(0..20) end - assert Enum.rotate([:a, :b, :c, :d, :e, :f], 1..3, 1) == [:a, :b, :c, :d, :e, :f] + assert Enum.slide([:a, :b, :c, :d, :e, :f], 1..3, 1) == [:a, :b, :c, :d, :e, :f] end - test "on the whole of a list reorders it correctly" do + test "on the whole of an enumerable reorders it correctly" do for zero_to_20 <- [0..20, Enum.to_list(0..20)] do expected_numbers = Enum.flat_map([10..20, 0..9], &Enum.to_list/1) - assert Enum.rotate(zero_to_20, 10..20, 0) == expected_numbers + assert Enum.slide(zero_to_20, 10..20, 0) == expected_numbers end - assert Enum.rotate([:a, :b, :c, :d, :e, :f], 4..5, 0) == [:e, :f, :a, :b, :c, :d] + assert Enum.slide([:a, :b, :c, :d, :e, :f], 4..5, 0) == [:e, :f, :a, :b, :c, :d] end test "raises when the insertion point is inside the range" do for zero_to_20 <- [0..20, Enum.to_list(0..20)] do assert_raise RuntimeError, fn -> - Enum.rotate(zero_to_20, 10..18, 14) + Enum.slide(zero_to_20, 10..18, 14) end end end - test "accepts range starts that are off the end of the list, returning the input list" do - assert Enum.rotate([], 1..5, 0) == [] + test "accepts range starts that are off the end of the enum, returning the input list" do + assert Enum.slide([], 1..5, 0) == [] for zero_to_20 <- [0..20, Enum.to_list(0..20)] do - assert Enum.rotate(zero_to_20, 21..25, 3) == Enum.to_list(0..20) + assert Enum.slide(zero_to_20, 21..25, 3) == Enum.to_list(0..20) end end - test "accepts range ends that are off the end of the list, truncating the rotated range" do + test "accepts range ends that are off the end of the enum, truncating the moved range" do for zero_to_10 <- [0..10, Enum.to_list(0..10)] do - assert Enum.rotate(zero_to_10, 8..15, 4) == Enum.rotate(zero_to_10, 8..10, 4) + assert Enum.slide(zero_to_10, 8..15, 4) == Enum.slide(zero_to_10, 8..10, 4) end end @@ -937,10 +937,10 @@ defmodule EnumTest do {10..20, 0} ] - for {rotation_range, insertion_point} <- test_specs do - rotation = &Enum.rotate(&1, rotation_range, insertion_point) - assert rotation.(list) == rotation.(set) - assert rotation.(list) == rotation.(range) + for {slide_range, insertion_point} <- test_specs do + slide = &Enum.slide(&1, slide_range, insertion_point) + assert slide.(list) == slide.(set) + assert slide.(list) == slide.(range) end end end From ec65d637d9d9c631c54c8d823782684556d436c0 Mon Sep 17 00:00:00 2001 From: "Tyler A. Young" Date: Sun, 31 Oct 2021 05:54:47 -0500 Subject: [PATCH 3/5] =?UTF-8?q?Docs=20changes=20from=20Jos=C3=A9's=20code?= =?UTF-8?q?=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/elixir/lib/enum.ex | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index 7d3a819e31a..ff67b49bafe 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -2533,22 +2533,25 @@ defmodule Enum do end @doc """ - Pulls out either a single element (denoted by an integer index) or a contiguous range of - the enumerable (given by a range) and inserts it in front of the value previously at the - insertion index. - - The semantics of the range to be moved match the semantics of Enum.slice/2. Specifically, - that means: - - * Indices are normalized, meaning that negative indexes will be counted from the end - (for example, -1 means the last element of the enumerable). This will result in *two* - traversals of your enumerable on types like lists that don't provide a constant-time count. - * If the normalized index range's `last` is out of bounds, the range is truncated to the last element. - * If the normalized index range's `first` is out of bounds, the selected range for sliding - will be empty, so you'll get back your input list. - * Decreasing ranges (e.g., the range 5..0//1) also select an empty range to be moved, so you'll - get back your input list. - * Ranges with any step but 1 will raise an error. + Slides a single or multiple elements given by `range_or_single_index` from `enumerable` + to `insertion_index`. + + The semantics of the range to be moved match the semantics of `Enum.slice/2`. + Specifically, that means: + + * Indices are normalized, meaning that negative indexes will be counted from the end + (for example, -1 means the last element of the enumerable). This will result in *two* + traversals of your enumerable on types like lists that don't provide a constant-time count. + + * If the normalized index range's `last` is out of bounds, the range is truncated to the last element. + + * If the normalized index range's `first` is out of bounds, the selected range for sliding + will be empty, so you'll get back your input list. + + * Decreasing ranges (such as `5..0//1`) also select an empty range to be moved, + so you'll get back your input list. + + * Ranges with any step but 1 will raise an error. ## Examples From 8122f805f9e6361db775b7aa67fad127499c2439 Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Sun, 31 Oct 2021 06:03:21 -0500 Subject: [PATCH 4/5] =?UTF-8?q?Doctest=20clarification=20and=20private=20f?= =?UTF-8?q?unction=20renames=20per=20Jos=C3=A9's=20code=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/elixir/lib/enum.ex | 46 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index ff67b49bafe..876dcd87ab3 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -2556,22 +2556,22 @@ defmodule Enum do ## Examples # Slide a single element - iex> Enum.slide([0, 1, 2, 3, 4, 5, 6], 5, 1) - [0, 5, 1, 2, 3, 4, 6] + iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 5, 1) + [:a, :f, :b, :c, :d, :e, :g] # Slide a range of elements backward - iex> Enum.slide([0, 1, 2, 3, 4, 5, 6], 3..5, 1) - [0, 3, 4, 5, 1, 2, 6] + iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 3..5, 1) + [:a, :d, :e, :f, :b, :c, :g] # Slide a range of elements forward - iex> Enum.slide([0, 1, 2, 3, 4, 5, 6], 1..3, 5) - [0, 4, 5, 1, 2, 3, 6] + iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 1..3, 5) + [:a, :e, :f, :b, :c, :d, :g] # Slide with negative indices (counting from the end) - iex> Enum.slide([0, 1, 2, 3, 4, 5, 6], 3..-1//1, 2) - [0, 1, 3, 4, 5, 6, 2] - iex> Enum.slide([0, 1, 2, 3, 4, 5, 6], -4..-2, 1) - [0, 3, 4, 5, 1, 2, 6] + iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 3..-1//1, 2) + [:a, :b, :d, :e, :f, :g, :c] + iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], -4..-2, 1) + [:a, :d, :e, :f, :b, :c, :g] """ def slide(enumerable, range_or_single_index, insertion_index) @@ -2612,7 +2612,7 @@ defmodule Enum do # Guarantees at this point: step size == 1 and first <= last and (insertion_index < first or insertion_index > last) def slide(enumerable, first..last, insertion_index) do - impl = if is_list(enumerable), do: &find_start/4, else: &slide_any/4 + impl = if is_list(enumerable), do: &slide_list_start/4, else: &slide_any/4 cond do insertion_index <= first -> impl.(enumerable, insertion_index, first, last) @@ -2652,36 +2652,36 @@ defmodule Enum do # Like slide_any/4 above, this optimized implementation of slide for lists depends # on the indices being sorted such that we're moving middle..last to be in front of start. - defp find_start([h | t], start, middle, last) + defp slide_list_start([h | t], start, middle, last) when start > 0 and start <= middle and middle <= last do - [h | find_start(t, start - 1, middle - 1, last - 1)] + [h | slide_list_start(t, start - 1, middle - 1, last - 1)] end - defp find_start(list, 0, middle, last), do: accumulate_start_middle(list, middle, last, []) + defp slide_list_start(list, 0, middle, last), do: slide_list_middle(list, middle, last, []) - defp accumulate_start_middle([h | t], middle, last, acc) when middle > 0 do - accumulate_start_middle(t, middle - 1, last - 1, [h | acc]) + defp slide_list_middle([h | t], middle, last, acc) when middle > 0 do + slide_list_middle(t, middle - 1, last - 1, [h | acc]) end - defp accumulate_start_middle(list, 0, last, start_to_middle) do - {slid_range, tail} = accumulate_middle_last(list, last + 1, []) + defp slide_list_middle(list, 0, last, start_to_middle) do + {slid_range, tail} = slide_list_last(list, last + 1, []) slid_range ++ :lists.reverse(start_to_middle, tail) end # You asked for a middle index off the end of the list... you get what we've got - defp accumulate_start_middle([], _, _, acc) do + defp slide_list_middle([], _, _, acc) do :lists.reverse(acc) end - defp accumulate_middle_last([h | t], last, acc) when last > 0 do - accumulate_middle_last(t, last - 1, [h | acc]) + defp slide_list_last([h | t], last, acc) when last > 0 do + slide_list_last(t, last - 1, [h | acc]) end - defp accumulate_middle_last(rest, 0, acc) do + defp slide_list_last(rest, 0, acc) do {:lists.reverse(acc), rest} end - defp accumulate_middle_last([], _, acc) do + defp slide_list_last([], _, acc) do {:lists.reverse(acc), []} end From cc7166d43e0106c9891d90af23cd6074083fb4b4 Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Sun, 31 Oct 2021 06:03:59 -0500 Subject: [PATCH 5/5] Typo fix and minor clarification on Enum.slide/4 test with MapSet --- lib/elixir/test/elixir/enum_test.exs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index 6e0054aa1c5..c2e1e965355 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -923,7 +923,10 @@ defmodule EnumTest do test "matches behavior for lists vs. ranges" do range = 0..20 list = Enum.to_list(range) - # Below 32 elements, the map implementation currently sticks the pairs these in order + # Below 32 elements, the map implementation currently sticks values in order. + # If ever the MapSet implementation changes, this will fail (not affecting the correctness + # of slide). I figured it'd be worth testing this for the time being just to have + # another enumerable (aside from range) testing the generic implementation. set = MapSet.new(list) test_specs = [