Skip to content

Commit f5cab4a

Browse files
committed
Merge pull request #1281 from bettio/add-more-enum-functions-1
Add more Enum functions Add some missing Enum functions and make find functions compatible with Enumerable protocol. These changes are made under both the "Apache 2.0" and the "GNU Lesser General Public License 2.1 or later" license terms (dual license). SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
2 parents 0d2cb6b + 6b5da55 commit f5cab4a

File tree

3 files changed

+256
-6
lines changed

3 files changed

+256
-6
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ Elixir standard library modules
1515
- Support for Elixir `String.Chars` protocol, now functions such as `Enum.join` are able to take
1616
also non string parameters (e.g. `Enum.join([1, 2], ",")`
1717
- Support for Elixir `Enum.at/3`
18-
- Add support to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?`, `Enum.each` and
19-
`Enum.filter`
2018
- Add support for `is_bitstring/1` construct which is used in Elixir protocols runtime.
19+
- Add support to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?`, `Enum.each`,
20+
`Enum.filter`, `Enum.flat_map`, `Enum.reject`, `Enum.chunk_by` and `Enum.chunk_while`
2121

2222
### Changed
2323

2424
- ESP32: Elixir library is not shipped anymore with `esp32boot.avm`. Use `elixir_esp32boot.avm`
2525
instead
26+
- `Enum.find_index` and `Enum.find_value` support Enumerable and not just lists
2627

2728
### Fixed
2829

libs/exavmlib/lib/Enum.ex

Lines changed: 214 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ defmodule Enum do
2424
@compile {:autoload, false}
2525

2626
@type t :: Enumerable.t()
27+
@type acc :: any
2728
@type index :: integer
2829
@type element :: any
2930

@@ -209,6 +210,98 @@ defmodule Enum do
209210
end
210211
end
211212

213+
@doc """
214+
Chunks the `enumerable` with fine grained control when every chunk is emitted.
215+
216+
`chunk_fun` receives the current element and the accumulator and
217+
must return `{:cont, chunk, acc}` to emit the given chunk and
218+
continue with accumulator or `{:cont, acc}` to not emit any chunk
219+
and continue with the return accumulator.
220+
221+
`after_fun` is invoked when iteration is done and must also return
222+
`{:cont, chunk, acc}` or `{:cont, acc}`.
223+
224+
Returns a list of lists.
225+
226+
## Examples
227+
228+
iex> chunk_fun = fn element, acc ->
229+
...> if rem(element, 2) == 0 do
230+
...> {:cont, Enum.reverse([element | acc]), []}
231+
...> else
232+
...> {:cont, [element | acc]}
233+
...> end
234+
...> end
235+
iex> after_fun = fn
236+
...> [] -> {:cont, []}
237+
...> acc -> {:cont, Enum.reverse(acc), []}
238+
...> end
239+
iex> Enum.chunk_while(1..10, [], chunk_fun, after_fun)
240+
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
241+
242+
"""
243+
@doc since: "1.5.0"
244+
@spec chunk_while(
245+
t,
246+
acc,
247+
(element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}),
248+
(acc -> {:cont, chunk, acc} | {:cont, acc})
249+
) :: Enumerable.t()
250+
when chunk: any
251+
def chunk_while(enumerable, acc, chunk_fun, after_fun) do
252+
{_, {res, acc}} =
253+
Enumerable.reduce(enumerable, {:cont, {[], acc}}, fn entry, {buffer, acc} ->
254+
case chunk_fun.(entry, acc) do
255+
{:cont, emit, acc} -> {:cont, {[emit | buffer], acc}}
256+
{:cont, acc} -> {:cont, {buffer, acc}}
257+
{:halt, acc} -> {:halt, {buffer, acc}}
258+
end
259+
end)
260+
261+
case after_fun.(acc) do
262+
{:cont, _acc} -> :lists.reverse(res)
263+
{:cont, elem, _acc} -> :lists.reverse([elem | res])
264+
end
265+
end
266+
267+
@doc """
268+
Splits enumerable on every element for which `fun` returns a new
269+
value.
270+
271+
Returns a list of lists.
272+
273+
## Examples
274+
275+
iex> Enum.chunk_by([1, 2, 2, 3, 4, 4, 6, 7, 7], &(rem(&1, 2) == 1))
276+
[[1], [2, 2], [3], [4, 4, 6], [7, 7]]
277+
278+
"""
279+
@spec chunk_by(t, (element -> any)) :: [list]
280+
def chunk_by(enumerable, fun) do
281+
reducers_chunk_by(&chunk_while/4, enumerable, fun)
282+
end
283+
284+
# Taken from Stream.Reducers
285+
defp reducers_chunk_by(chunk_by, enumerable, fun) do
286+
chunk_fun = fn
287+
entry, nil ->
288+
{:cont, {[entry], fun.(entry)}}
289+
290+
entry, {acc, value} ->
291+
case fun.(entry) do
292+
^value -> {:cont, {[entry | acc], value}}
293+
new_value -> {:cont, :lists.reverse(acc), {[entry], new_value}}
294+
end
295+
end
296+
297+
after_fun = fn
298+
nil -> {:cont, :done}
299+
{acc, _value} -> {:cont, :lists.reverse(acc), :done}
300+
end
301+
302+
chunk_by.(enumerable, nil, chunk_fun, after_fun)
303+
end
304+
212305
@doc """
213306
Invokes the given `fun` for each element in the `enumerable`.
214307
@@ -305,14 +398,101 @@ defmodule Enum do
305398
|> elem(1)
306399
end
307400

401+
@doc """
402+
Similar to `find/3`, but returns the index (zero-based)
403+
of the element instead of the element itself.
404+
405+
## Examples
406+
407+
iex> Enum.find_index([2, 4, 6], fn x -> rem(x, 2) == 1 end)
408+
nil
409+
410+
iex> Enum.find_index([2, 3, 4], fn x -> rem(x, 2) == 1 end)
411+
1
412+
413+
"""
414+
@spec find_index(t, (element -> any)) :: non_neg_integer | nil
308415
def find_index(enumerable, fun) when is_list(enumerable) do
309416
find_index_list(enumerable, 0, fun)
310417
end
311418

419+
def find_index(enumerable, fun) do
420+
result =
421+
Enumerable.reduce(enumerable, {:cont, {:not_found, 0}}, fn entry, {_, index} ->
422+
if fun.(entry), do: {:halt, {:found, index}}, else: {:cont, {:not_found, index + 1}}
423+
end)
424+
425+
case elem(result, 1) do
426+
{:found, index} -> index
427+
{:not_found, _} -> nil
428+
end
429+
end
430+
431+
@doc """
432+
Similar to `find/3`, but returns the value of the function
433+
invocation instead of the element itself.
434+
435+
## Examples
436+
437+
iex> Enum.find_value([2, 4, 6], fn x -> rem(x, 2) == 1 end)
438+
nil
439+
440+
iex> Enum.find_value([2, 3, 4], fn x -> rem(x, 2) == 1 end)
441+
true
442+
443+
iex> Enum.find_value([1, 2, 3], "no bools!", &is_boolean/1)
444+
"no bools!"
445+
446+
"""
447+
@spec find_value(t, any, (element -> any)) :: any | nil
448+
def find_value(enumerable, default \\ nil, fun)
449+
312450
def find_value(enumerable, default, fun) when is_list(enumerable) do
313451
find_value_list(enumerable, default, fun)
314452
end
315453

454+
def find_value(enumerable, default, fun) do
455+
Enumerable.reduce(enumerable, {:cont, default}, fn entry, default ->
456+
fun_entry = fun.(entry)
457+
if fun_entry, do: {:halt, fun_entry}, else: {:cont, default}
458+
end)
459+
|> elem(1)
460+
end
461+
462+
@doc """
463+
Maps the given `fun` over `enumerable` and flattens the result.
464+
465+
This function returns a new enumerable built by appending the result of invoking `fun`
466+
on each element of `enumerable` together; conceptually, this is similar to a
467+
combination of `map/2` and `concat/1`.
468+
469+
## Examples
470+
471+
iex> Enum.flat_map([:a, :b, :c], fn x -> [x, x] end)
472+
[:a, :a, :b, :b, :c, :c]
473+
474+
iex> Enum.flat_map([{1, 3}, {4, 6}], fn {x, y} -> x..y end)
475+
[1, 2, 3, 4, 5, 6]
476+
477+
iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end)
478+
[[:a], [:b], [:c]]
479+
480+
"""
481+
@spec flat_map(t, (element -> t)) :: list
482+
def flat_map(enumerable, fun) when is_list(enumerable) do
483+
flat_map_list(enumerable, fun)
484+
end
485+
486+
def flat_map(enumerable, fun) do
487+
reduce(enumerable, [], fn entry, acc ->
488+
case fun.(entry) do
489+
list when is_list(list) -> :lists.reverse(list, acc)
490+
other -> reduce(other, acc, &[&1 | &2])
491+
end
492+
end)
493+
|> :lists.reverse()
494+
end
495+
316496
@doc """
317497
Returns a list where each element is the result of invoking
318498
`fun` on each corresponding element of `enumerable`.
@@ -415,10 +595,6 @@ defmodule Enum do
415595
end
416596
end
417597

418-
def reject(enumerable, fun) when is_list(enumerable) do
419-
reject_list(enumerable, fun)
420-
end
421-
422598
## all?
423599

424600
defp all_list([h | t], fun) do
@@ -499,6 +675,19 @@ defmodule Enum do
499675
default
500676
end
501677

678+
## flat_map
679+
680+
defp flat_map_list([head | tail], fun) do
681+
case fun.(head) do
682+
list when is_list(list) -> list ++ flat_map_list(tail, fun)
683+
other -> to_list(other) ++ flat_map_list(tail, fun)
684+
end
685+
end
686+
687+
defp flat_map_list([], _fun) do
688+
[]
689+
end
690+
502691
@doc """
503692
Inserts the given `enumerable` into a `collectable`.
504693
@@ -650,6 +839,27 @@ defmodule Enum do
650839
[]
651840
end
652841

842+
@doc """
843+
Returns a list of elements in `enumerable` excluding those for which the function `fun` returns
844+
a truthy value.
845+
846+
See also `filter/2`.
847+
848+
## Examples
849+
850+
iex> Enum.reject([1, 2, 3], fn x -> rem(x, 2) == 0 end)
851+
[1, 3]
852+
853+
"""
854+
@spec reject(t, (element -> as_boolean(term))) :: list
855+
def reject(enumerable, fun) when is_list(enumerable) do
856+
reject_list(enumerable, fun)
857+
end
858+
859+
def reject(enumerable, fun) do
860+
reduce(enumerable, [], R.reject(fun)) |> :lists.reverse()
861+
end
862+
653863
@doc """
654864
Returns a list of elements in `enumerable` in reverse order.
655865

tests/libs/exavmlib/Tests.ex

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,12 @@ defmodule Tests do
3939
[2, 3] = Enum.slice([1, 2, 3], 1, 2)
4040
:test = Enum.at([0, 1, :test, 3], 2)
4141
:atom = Enum.find([1, 2, :atom, 3, 4], -1, fn item -> not is_integer(item) end)
42+
1 = Enum.find_index([:a, :b, :c], fn item -> item == :b end)
43+
true = Enum.find_value([-2, -3, -1, 0, 1], fn item -> item >= 0 end)
4244
true = Enum.all?([1, 2, 3], fn n -> n >= 0 end)
4345
true = Enum.any?([1, -2, 3], fn n -> n < 0 end)
4446
[2] = Enum.filter([1, 2, 3], fn n -> rem(n, 2) == 0 end)
47+
[1, 3] = Enum.reject([1, 2, 3], fn n -> rem(n, 2) == 0 end)
4548
:ok = Enum.each([1, 2, 3], fn n -> true = is_integer(n) end)
4649

4750
# map
@@ -63,9 +66,11 @@ defmodule Tests do
6366
true = at_0 != at_1
6467
{:c, :atom} = Enum.find(%{a: 1, b: 2, c: :atom, d: 3}, fn {_k, v} -> not is_integer(v) end)
6568
{:d, 3} = Enum.find(%{a: 1, b: 2, c: :atom, d: 3}, fn {k, _v} -> k == :d end)
69+
true = Enum.find_value(%{"a" => 1, b: 2}, fn {k, _v} -> is_atom(k) end)
6670
true = Enum.all?(%{a: 1, b: 2}, fn {_k, v} -> v >= 0 end)
6771
true = Enum.any?(%{a: 1, b: -2}, fn {_k, v} -> v < 0 end)
6872
[b: 2] = Enum.filter(%{a: 1, b: 2, c: 3}, fn {_k, v} -> rem(v, 2) == 0 end)
73+
[] = Enum.reject(%{a: 1, b: 2, c: 3}, fn {_k, v} -> v > 0 end)
6974
:ok = Enum.each(%{a: 1, b: 2}, fn {_k, v} -> true = is_integer(v) end)
7075

7176
# map set
@@ -80,9 +85,11 @@ defmodule Tests do
8085
true = ms_at_0 == 1 or ms_at_0 == 2
8186
true = ms_at_1 == 1 or ms_at_1 == 2
8287
:atom = Enum.find(MapSet.new([1, 2, :atom, 3, 4]), fn item -> not is_integer(item) end)
88+
nil = Enum.find_value([-2, -3, -1, 0, 1], fn item -> item > 100 end)
8389
true = Enum.all?(MapSet.new([1, 2, 3]), fn n -> n >= 0 end)
8490
true = Enum.any?(MapSet.new([1, -2, 3]), fn n -> n < 0 end)
8591
[2] = Enum.filter(MapSet.new([1, 2, 3]), fn n -> rem(n, 2) == 0 end)
92+
[1] = Enum.reject(MapSet.new([1, 2, 3]), fn n -> n > 1 end)
8693
:ok = Enum.each(MapSet.new([1, 2, 3]), fn n -> true = is_integer(n) end)
8794

8895
# range
@@ -94,9 +101,11 @@ defmodule Tests do
94101
[6, 7, 8, 9, 10] = Enum.slice(1..10, 5, 100)
95102
7 = Enum.at(1..10, 6)
96103
8 = Enum.find(-10..10, fn item -> item >= 8 end)
104+
true = Enum.find_value(-10..10, fn item -> item >= 0 end)
97105
true = Enum.all?(0..10, fn n -> n >= 0 end)
98106
true = Enum.any?(-1..10, fn n -> n < 0 end)
99107
[0, 1, 2] = Enum.filter(-10..2, fn n -> n >= 0 end)
108+
[-1] = Enum.reject(-1..10, fn n -> n >= 0 end)
100109
:ok = Enum.each(-5..5, fn n -> true = is_integer(n) end)
101110

102111
# into
@@ -105,6 +114,11 @@ defmodule Tests do
105114
expected_mapset = MapSet.new([1, 2, 3])
106115
^expected_mapset = Enum.into([1, 2, 3], MapSet.new())
107116

117+
# Enum.flat_map
118+
[:a, :a, :b, :b, :c, :c] = Enum.flat_map([:a, :b, :c], fn x -> [x, x] end)
119+
[1, 2, 3, 4, 5, 6] = Enum.flat_map([{1, 3}, {4, 6}], fn {x, y} -> x..y end)
120+
[[:a], [:b], [:c]] = Enum.flat_map([:a, :b, :c], fn x -> [[x]] end)
121+
108122
# Enum.join
109123
"1, 2, 3" = Enum.join(["1", "2", "3"], ", ")
110124
"1, 2, 3" = Enum.join([1, 2, 3], ", ")
@@ -113,6 +127,9 @@ defmodule Tests do
113127
# Enum.reverse
114128
[4, 3, 2] = Enum.reverse([2, 3, 4])
115129

130+
# other enum functions
131+
test_enum_chunk_while()
132+
116133
undef =
117134
try do
118135
Enum.map({1, 2}, fn x -> x end)
@@ -132,6 +149,28 @@ defmodule Tests do
132149
:ok
133150
end
134151

152+
defp test_enum_chunk_while() do
153+
initial_col = 4
154+
lines_list = '-1234567890\nciao\n12345\nabcdefghijkl\n12'
155+
columns = 5
156+
157+
chunk_fun = fn char, {count, rchars} ->
158+
cond do
159+
char == ?\n -> {:cont, Enum.reverse(rchars), {0, []}}
160+
count == columns -> {:cont, Enum.reverse(rchars), {1, [char]}}
161+
true -> {:cont, {count + 1, [char | rchars]}}
162+
end
163+
end
164+
165+
after_fun = fn
166+
{_count, []} -> {:cont, [], []}
167+
{_count, rchars} -> {:cont, Enum.reverse(rchars), []}
168+
end
169+
170+
['-', '12345', '67890', 'ciao', '12345', 'abcde', 'fghij', 'kl', '12'] =
171+
Enum.chunk_while(lines_list, {initial_col, []}, chunk_fun, after_fun)
172+
end
173+
135174
defp test_exception() do
136175
ex1 =
137176
try do

0 commit comments

Comments
 (0)