Skip to content

Commit 9505373

Browse files
Retain enumeration properties when using to_rows on row data (#9)
1 parent 225ac4c commit 9505373

3 files changed

Lines changed: 81 additions & 1 deletion

File tree

lib/table.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ defmodule Table do
104104
end
105105

106106
defp read_rows({:rows, meta, enum}, only) do
107-
Stream.map(enum, fn values ->
107+
Table.Mapper.map(enum, fn values ->
108108
build_row(meta.columns, values, only)
109109
end)
110110
end

lib/table/mapper.ex

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
defmodule Table.Mapper do
2+
@moduledoc false
3+
4+
# An enumerable that maps a function over another enumerable.
5+
#
6+
# This enumerable proxies traversal to the underlying enumerable,
7+
# so it keeps the same properties, such as optimized slicing.
8+
9+
defstruct [:enumerable, :mapper]
10+
11+
@doc """
12+
Returns an enumerable that will apply the given function on
13+
enumeration.
14+
"""
15+
@spec map(Enumerable.t(), (any() -> any())) :: Enumerable.t()
16+
def map(enumerable, fun) do
17+
%__MODULE__{enumerable: enumerable, mapper: fun}
18+
end
19+
20+
defimpl Enumerable do
21+
def count(proxy) do
22+
Enumerable.count(proxy.enumerable)
23+
end
24+
25+
def member?(proxy, element) do
26+
# The mapping is not necessarily reversible, so we fall
27+
# back to a linear search. For enumerables representing
28+
# data entries member? would generally be linear anyway
29+
Enum.any?(proxy.enumerable, fn original ->
30+
proxy.mapper.(original) == element
31+
end)
32+
end
33+
34+
def reduce(proxy, acc, fun) do
35+
Enumerable.reduce(proxy.enumerable, acc, fn original, acc ->
36+
fun.(proxy.mapper.(original), acc)
37+
end)
38+
end
39+
40+
def slice(proxy) do
41+
case Enumerable.slice(proxy.enumerable) do
42+
{:ok, size, fun} ->
43+
fun =
44+
case fun do
45+
to_list_fun when is_function(to_list_fun, 1) ->
46+
&(to_list_fun.(&1) |> Enum.map(proxy.mapper))
47+
48+
slicing_fun when is_function(slicing_fun, 2) ->
49+
&(slicing_fun.(&1, &2) |> Enum.map(proxy.mapper))
50+
51+
slicing_fun when is_function(slicing_fun, 3) ->
52+
&(slicing_fun.(&1, &2, &3) |> Enum.map(proxy.mapper))
53+
end
54+
55+
{:ok, size, fun}
56+
57+
{:error, _module} ->
58+
{:error, __MODULE__}
59+
end
60+
end
61+
end
62+
end

test/mapper_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
defmodule Table.MapperTest do
2+
use ExUnit.Case, async: true
3+
4+
alias Table.Mapper
5+
6+
test "reduce" do
7+
enumerable = 1..3 |> Mapper.map(fn x -> x * x end)
8+
assert Enum.reduce(enumerable, &(&1 + &2)) == 14
9+
end
10+
11+
test "slice" do
12+
enumerable = 1..10 |> Mapper.map(fn x -> x * x end)
13+
assert Enum.slice(enumerable, 4..6) == [25, 36, 49]
14+
15+
enumerable = 1..10 |> Stream.map(& &1) |> Mapper.map(fn x -> x * x end)
16+
assert Enum.slice(enumerable, 4..6) == [25, 36, 49]
17+
end
18+
end

0 commit comments

Comments
 (0)