Skip to content

Commit

Permalink
Support for Arrays.slice!
Browse files Browse the repository at this point in the history
  • Loading branch information
Qqwy committed Sep 4, 2021
1 parent 15465e7 commit 92eea2e
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 3 deletions.
96 changes: 96 additions & 0 deletions lib/arrays.ex
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,102 @@ contents = quote do
enumerable_of_enumerables
|> Enum.reduce(Arrays.new(), &Enum.into/2)
end

@doc """
Returns a sub-array of the given array by `index_range`.
`index_range` must be a `Range`. Given an array, it drops elements before
`index_range.first` (zero-based), then it takes elements until element
`index_range.last` (inclusively).
Indexes are normalized, meaning that negative indexes will be counted from the
end (for example, -1 means the last element of the array).
If `index_range.last` is out of bounds, then it is assigned as the index of the
last element.
If the normalized `index_range.first` is out of bounds of the given array,
or if it is is greater than the normalized `index_range.last`,
then an empty array is returned.
iex> Arrays.slice(Arrays.new([1, 2, 3, 4, 5, 6]), 2..4)
##{@current_default_array}<[3, 4, 5]>
See also `slice/3`.
Compare with `Enum.slice/2`.
"""
@spec slice(array, Range.t()) :: array
def slice(array, index_range)

@doc """
Returns a sub-array of the given array, from `start_index` (zero-based)
with `amount` number of elements if available.
Given an array, it skips elements right before element `start_index`; then,
it takes `amount` of elements, returning as many elements as possible if there
are not enough elements.
A negative `start_index` can be passed, which means the array is enumerated
once and the index is counted from the end (for example, -1 starts slicing from
the last element).
It returns an empty array if `amount` is 0 or if `start_index` is out of bounds.
See also `slice/2`.
Compare with `Enum.slice/3`.
"""
@spec slice(array, index, non_neg_integer) :: array
def slice(array, start_index, amount)

# NOTE: we are not using the new range step syntax here
# for backwards-compatibility with older Elixir versions.
def slice(array, index_range = %{first: first, last: last, step: step}) do
if step == 1 or (step == -1 and first > last) do
slice(array, first, last)
else
raise ArgumentError,
"Arrays.slice/2 does not accept ranges with custom steps, got: #{inspect(index_range)}"
end
end
def slice(array, index_range = %{__struct__: Range, first: first, last: last}) do
step = if first <= last, do: 1, else: -1
slice(array, Map.put(index_range, :step, step))
end

def slice(array, first, last) when last >= first and last >= 0 and first >= 0 do
slice_any(array, first, last - first + 1)
end

def slice(array, first, last) do
count = Arrays.Protocol.size(array)
first = if first >= 0, do: first, else: first + count
last = if last >= 0, do: last, else: last + count
amount = last - first + 1

if first >= 0 and first < count and amount > 0 do
Arrays.Protocol.slice(array, first, min(amount, count - first))
else
Arrays.new()
end
end

defp slice_any(array, start, amount) when start < 0 do
count = Arrays.Protocol.size(array)
start = count + start
Arrays.Protocol.slice(array, start, min(amount, count - start))
end

defp slice_any(array, start, amount) when start >= 0 do
count = Arrays.Protocol.size(array)
if start >= count do
Arrays.new()
else
Arrays.Protocol.slice(array, start, min(amount, count - start))
end
end
end

Module.create(Arrays,
Expand Down
5 changes: 3 additions & 2 deletions lib/arrays/implementations/common_protocol_implementations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ defmodule Arrays.CommonProtocolImplementations do
end

def slice(array) do
# size = Arrays.Protocol.size(array)
size = Arrays.Protocol.size(array)
# {:ok, size, &Enumerable.List.slice(Arrays.Protocol.to_list(array), &1, &2, size)}
# TODO write a faster implementation that is correct
{:error, __MODULE__}
# {:error, __MODULE__}
{:ok, size, &@for.build_slice(array, &1, &2, [])}
end
end
end
Expand Down
12 changes: 12 additions & 0 deletions lib/arrays/implementations/erlang_array.ex
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ defmodule Arrays.Implementations.ErlangArray do
:array.foldl(fun, acc, arr)
end

@doc false
def build_slice(%ErlangArray{contents: contents}, start, length, into) do
for index <- start..(start + length - 1), into: into do
:array.get(index, contents)
end
end

defimpl Arrays.Protocol do
alias Arrays.Implementations.ErlangArray

Expand Down Expand Up @@ -244,5 +251,10 @@ defmodule Arrays.Implementations.ErlangArray do
def to_list(%ErlangArray{contents: contents}) do
:array.to_list(contents)
end

@impl true
def slice(array = %ErlangArray{}, start, amount) do
@for.build_slice(array, start, amount, @for.empty(default: :array.default(array.contents)))
end
end
end
12 changes: 12 additions & 0 deletions lib/arrays/implementations/map_array.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ defmodule Arrays.Implementations.MapArray do
end
end

@doc false
def build_slice(%MapArray{contents: contents}, start, length, into) do
for index <- start..(start + length - 1), into: into do
contents[index]
end
end

defimpl Arrays.Protocol do
alias Arrays.Implementations.MapArray

Expand Down Expand Up @@ -240,5 +247,10 @@ defmodule Arrays.Implementations.MapArray do
def to_list(%MapArray{contents: contents}) do
:maps.values(contents)
end

@impl true
def slice(array = %MapArray{}, start, amount) do
@for.build_slice(array, start, amount, @for.empty(default: array.default))
end
end
end
11 changes: 10 additions & 1 deletion lib/arrays/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ defprotocol Arrays.Protocol do
Do not call functions in this module directly if you want to use an array in your code.
Instead, use the functions in the `Arrays` module, which will use the methods of this protocol
(as well as the `Arrays.Behaviour` behaviour) internally.
"""
@typedoc """
Any datatype implementing the `Arrays.Protocol` as well as the `Arrays.Behaviour`.
Expand Down Expand Up @@ -123,4 +122,14 @@ defprotocol Arrays.Protocol do
"""
@spec to_list(array) :: list
def to_list(array)

@doc """
Return a contiguous slice of some elements in the array.
Handling of bounds is handled in the `Arrays` module,
so we know for certain that `0 <= start_index < size(array)`
and `start_index + length < size(array)`.
"""
@spec slice(array, index, non_neg_integer) :: array
def slice(array, start_index, amount)
end

0 comments on commit 92eea2e

Please sign in to comment.