Skip to content

Commit 2d7965b

Browse files
author
José Valim
committed
Add Stream.chunks/2,3,4
1 parent 4a2d1d6 commit 2d7965b

File tree

4 files changed

+102
-28
lines changed

4 files changed

+102
-28
lines changed

lib/elixir/lib/enum.ex

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -311,15 +311,12 @@ defmodule Enum do
311311
312312
"""
313313
@spec chunks(t, non_neg_integer, non_neg_integer) :: [list]
314-
@spec chunks(t, non_neg_integer, non_neg_integer, list | nil) :: [list]
314+
@spec chunks(t, non_neg_integer, non_neg_integer, t | nil) :: [list]
315315
def chunks(coll, n, step, pad // nil) when n > 0 and step > 0 do
316-
{ acc, buffer, i } =
317-
reduce(coll, { [], [], 0 }, fn
318-
x, { acc, buffer, i } when i < n ->
319-
chunks_n(acc, [x|buffer], i + 1, n, step)
320-
x, { acc, buffer, i } when i < step ->
321-
chunks_step(acc, [x|buffer], i + 1, step)
322-
end)
316+
limit = :erlang.max(n, step)
317+
318+
{ _, { acc, { buffer, i } } } =
319+
Enumerable.reduce(coll, { :cont, { [], { [], 0 } } }, R.chunks(n, step, limit))
323320

324321
if nil?(pad) || i == 0 do
325322
:lists.reverse(acc)
@@ -1599,25 +1596,7 @@ defmodule Enum do
15991596

16001597
## Helpers
16011598

1602-
@compile { :inline, chunks_n: 5, chunks_step: 4, to_string: 2 }
1603-
1604-
defp chunks_n(acc, buffer, i, n, step) when i == n do
1605-
acc = [:lists.reverse(buffer)|acc]
1606-
chunks_step(acc, buffer, i, step)
1607-
end
1608-
1609-
defp chunks_n(acc, buffer, i, _n, _step) do
1610-
{ acc, buffer, i }
1611-
end
1612-
1613-
defp chunks_step(acc, buffer, i, step) when i >= step do
1614-
left = i - step
1615-
{ acc, take(buffer, left), left }
1616-
end
1617-
1618-
defp chunks_step(acc, buffer, i, _step) do
1619-
{ acc, buffer, i }
1620-
end
1599+
@compile { :inline, to_string: 2 }
16211600

16221601
defp enumerate_and_count(collection, count) when is_list(collection) do
16231602
{ collection, length(collection) - abs(count) }

lib/elixir/lib/stream.ex

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,56 @@ defmodule Stream do
163163
## Transformers
164164

165165
@doc """
166-
Chunk the `enum` by buffering elements for which `fun` returns
166+
Shortcut to `chunks(coll, n, n)`.
167+
"""
168+
@spec chunks(Enumerable.t, non_neg_integer) :: Enumerable.t
169+
def chunks(coll, n), do: chunks(coll, n, n, nil)
170+
171+
@doc """
172+
Streams the enumerable in chunks, containing `n` items each, where
173+
each new chunk starts `step` elements into the enumerable.
174+
175+
`step` is optional and, if not passed, defaults to `n`, i.e.
176+
chunks do not overlap. If the final chunk does not have `n`
177+
elements to fill the chunk, elements are taken as necessary
178+
from `pad` if it was passed. If `pad` is passed and does not
179+
have enough elements to fill the chunk, then the chunk is
180+
returned anyway with less than `n` elements. If `pad` is not
181+
passed at all or is nil, then the partial chunk is discarded
182+
from the result.
183+
184+
## Examples
185+
186+
iex> Stream.chunks([1, 2, 3, 4, 5, 6], 2) |> Enum.to_list
187+
[[1, 2], [3, 4], [5, 6]]
188+
iex> Stream.chunks([1, 2, 3, 4, 5, 6], 3, 2) |> Enum.to_list
189+
[[1, 2, 3], [3, 4, 5]]
190+
iex> Stream.chunks([1, 2, 3, 4, 5, 6], 3, 2, [7]) |> Enum.to_list
191+
[[1, 2, 3], [3, 4, 5], [5, 6, 7]]
192+
iex> Stream.chunks([1, 2, 3, 4, 5, 6], 3, 3, []) |> Enum.to_list
193+
[[1, 2, 3], [4, 5, 6]]
194+
195+
"""
196+
@spec chunks(Enumerable.t, non_neg_integer, non_neg_integer) :: Enumerable.t
197+
@spec chunks(Enumerable.t, non_neg_integer, non_neg_integer, Enumerable.t | nil) :: Enumerable.t
198+
def chunks(enum, n, step, pad // nil) when n > 0 and step > 0 do
199+
limit = :erlang.max(n, step)
200+
lazy enum, { [], 0 },
201+
fn(f1) -> R.chunks(n, step, limit, f1) end,
202+
fn(f1) -> &do_chunks(&1, n, pad, f1) end
203+
end
204+
205+
defp do_chunks(acc(h, { buffer, count } = old, t) = acc, n, pad, f1) do
206+
if nil?(pad) || count == 0 do
207+
{ :cont, acc }
208+
else
209+
buffer = :lists.reverse(buffer) ++ Enum.take(pad, n - count)
210+
cont_with_acc(f1, buffer, h, old, t)
211+
end
212+
end
213+
214+
@doc """
215+
Chunks the `enum` by buffering elements for which `fun` returns
167216
the same value and only emit them when `fun` returns a new value
168217
or the `enum` finishes,
169218

lib/elixir/lib/stream/reducers.ex

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,29 @@ defmodule Stream.Reducers do
22
# Collection of reducers shared by Enum and Stream.
33
@moduledoc false
44

5+
defmacro chunks(n, step, limit, f // nil) do
6+
quote do
7+
fn entry, acc(h, { buffer, count }, t) ->
8+
buffer = [entry|buffer]
9+
count = count + 1
10+
11+
new =
12+
if count >= unquote(limit) do
13+
left = count - unquote(step)
14+
{ Enum.take(buffer, left), left }
15+
else
16+
{ buffer, count }
17+
end
18+
19+
if count == unquote(n) do
20+
cont_with_acc(unquote(f), :lists.reverse(buffer), h, new, t)
21+
else
22+
{ :cont, acc(h, new, t) }
23+
end
24+
end
25+
end
26+
end
27+
528
defmacro chunks_by(fun, f // nil) do
629
quote do
730
fn

lib/elixir/test/elixir/stream_test.exs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,29 @@ defmodule StreamTest do
2525
assert Enum.to_list(stream) == [3,5,7]
2626
end
2727

28+
test "chunks" do
29+
assert Stream.chunks([1, 2, 3, 4, 5], 2) |> Enum.to_list ==
30+
[[1, 2], [3, 4]]
31+
assert Stream.chunks([1, 2, 3, 4, 5], 2, 2, [6]) |> Enum.to_list ==
32+
[[1, 2], [3, 4], [5, 6]]
33+
assert Stream.chunks([1, 2, 3, 4, 5, 6], 3, 2) |> Enum.to_list ==
34+
[[1, 2, 3], [3, 4, 5]]
35+
assert Stream.chunks([1, 2, 3, 4, 5, 6], 2, 3) |> Enum.to_list ==
36+
[[1, 2], [4, 5]]
37+
assert Stream.chunks([1, 2, 3, 4, 5, 6], 3, 2, []) |> Enum.to_list ==
38+
[[1, 2, 3], [3, 4, 5], [5, 6]]
39+
assert Stream.chunks([1, 2, 3, 4, 5, 6], 3, 3, []) |> Enum.to_list ==
40+
[[1, 2, 3], [4, 5, 6]]
41+
assert Stream.chunks([1, 2, 3, 4, 5], 4, 4, 6..10) |> Enum.to_list ==
42+
[[1, 2, 3, 4], [5, 6, 7, 8]]
43+
end
44+
45+
test "chunks is zippable" do
46+
stream = Stream.chunks([1, 2, 3, 4, 5, 6], 3, 2, [])
47+
list = Enum.to_list(stream)
48+
assert Enum.zip(list, list) == Enum.zip(stream, stream)
49+
end
50+
2851
test "chunks_by" do
2952
stream = Stream.chunks_by([1, 2, 2, 3, 4, 4, 6, 7, 7], &(rem(&1, 2) == 1))
3053

0 commit comments

Comments
 (0)