Skip to content

Commit 303789f

Browse files
author
José Valim
committed
Support Stream.take/2 with negative count
1 parent 3c64178 commit 303789f

File tree

3 files changed

+59
-20
lines changed

3 files changed

+59
-20
lines changed

lib/elixir/lib/enum.ex

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,10 +1433,10 @@ defmodule Enum do
14331433
@doc """
14341434
Takes the first `count` items from the collection.
14351435
1436-
If a negative value `count` is given, the last `count`
1437-
values will be taken. The collection is enumerated
1438-
once to retrieve the proper index. The remaining
1439-
calculation is performed from the end.
1436+
If a negative `count` is given, the last `count` values will
1437+
be taken. For such, the collection is fully enumerated keeping up
1438+
to `2 * count` elements in memory. Once the end of the collection is
1439+
reached, the last `count` elements are returned.
14401440
14411441
## Examples
14421442
@@ -1473,7 +1473,8 @@ defmodule Enum do
14731473
end
14741474

14751475
def take(collection, count) when count < 0 do
1476-
do_take_reverse(reverse(collection), abs(count), [])
1476+
Stream.take(collection, count).({ :cont, [] }, &{ :cont, [&1|&2] })
1477+
|> elem(1) |> :lists.reverse
14771478
end
14781479

14791480
@doc """
@@ -1913,18 +1914,6 @@ defmodule Enum do
19131914
[]
19141915
end
19151916

1916-
defp do_take_reverse([h|t], counter, acc) when counter > 0 do
1917-
do_take_reverse(t, counter - 1, [h|acc])
1918-
end
1919-
1920-
defp do_take_reverse(_list, 0, acc) do
1921-
acc
1922-
end
1923-
1924-
defp do_take_reverse([], _, acc) do
1925-
acc
1926-
end
1927-
19281917
## take_while
19291918

19301919
defp do_take_while([h|t], fun) do

lib/elixir/lib/stream.ex

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,9 @@ defmodule Stream do
231231
232232
## Examples
233233
234-
iex> stream = Stream.filter_map([1, 2, 3], fn(x) -> rem(x, 2) == 0 end)
234+
iex> stream = Stream.filter_map(1..6, fn(x) -> rem(x, 2) == 0 end, &(&1 * 2))
235235
iex> Enum.to_list(stream)
236-
[2]
236+
[4,8,12]
237237
238238
"""
239239
@spec filter_map(Enumerable.t, (element -> as_boolean(term)), (element -> any)) :: Enumerable.t
@@ -332,23 +332,62 @@ defmodule Stream do
332332
Lazily takes the next `n` items from the enumerable and stops
333333
enumeration.
334334
335+
If a negative `n` is given, the last `n` values will be taken.
336+
For such, the collection is fully enumerated keeping up to `2 * n`
337+
elements in memory. Once the end of the collection is reached,
338+
the last `count` elements will be executed. Therefore, using
339+
a negative `n` in an infinite collection will never return.
340+
335341
## Examples
336342
337343
iex> stream = Stream.take(1..100, 5)
338344
iex> Enum.to_list(stream)
339345
[1,2,3,4,5]
340346
347+
iex> stream = Stream.take(1..100, -5)
348+
iex> Enum.to_list(stream)
349+
[96,97,98,99,100]
350+
341351
iex> stream = Stream.cycle([1, 2, 3]) |> Stream.take(5)
342352
iex> Enum.to_list(stream)
343353
[1,2,3,1,2]
344354
345355
"""
346356
@spec take(Enumerable.t, non_neg_integer) :: Enumerable.t
357+
def take(_enum, 0), do: Lazy[enum: []]
358+
347359
def take(enum, n) when n > 0 do
348360
lazy enum, n, fn(f1) -> R.take(f1) end
349361
end
350362

351-
def take(_enum, 0), do: Lazy[enum: []]
363+
def take(enum, n) when n < 0 do
364+
&do_take(enum, abs(n), &1, &2)
365+
end
366+
367+
def do_take(enum, n, acc, f) do
368+
{ _, { _count, buf1, buf2 } } =
369+
Enumerable.reduce(enum, { :cont, { 0, [], [] } }, fn
370+
entry, { count, buf1, buf2 } ->
371+
buf1 = [entry|buf1]
372+
count = count + 1
373+
if count == n do
374+
{ :cont, { 0, [], buf1 } }
375+
else
376+
{ :cont, { count, buf1, buf2} }
377+
end
378+
end)
379+
380+
Enumerable.reduce(do_take_last(buf1, buf2, n, []), acc, f)
381+
end
382+
383+
defp do_take_last(_buf1, _buf2, 0, acc),
384+
do: acc
385+
defp do_take_last([], [], _, acc),
386+
do: acc
387+
defp do_take_last([], [h|t], n, acc),
388+
do: do_take_last([], t, n-1, [h|acc])
389+
defp do_take_last([h|t], buf2, n, acc),
390+
do: do_take_last(t, buf2, n-1, [h|acc])
352391

353392
@doc """
354393
Creates a stream that takes every `n` item from the enumerable.

lib/elixir/test/elixir/stream_test.exs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,17 @@ defmodule StreamTest do
232232
assert Enum.to_list(stream) == [5,6,7,8,9,10,11,12,13,14,15]
233233
end
234234

235+
test "take with negative count" do
236+
Process.put(:stream_each, [])
237+
238+
stream = Stream.take(1..100, -5)
239+
assert is_lazy(stream)
240+
241+
stream = Stream.each(stream, &Process.put(:stream_each, [&1|Process.get(:stream_each)]))
242+
assert Enum.to_list(stream) == [96,97,98,99,100]
243+
assert Process.get(:stream_each) == [100,99,98,97,96]
244+
end
245+
235246
test "take is zippable" do
236247
stream = Stream.take(1..1000, 5)
237248
list = Enum.to_list(stream)

0 commit comments

Comments
 (0)