README.md

# ExArray

[![Coverage Status](https://coveralls.io/repos/github/tajacks/ex-array/badge.svg?branch=main)](https://coveralls.io/github/tajacks/ex-array?branch=main)

[![Build and Test](https://github.com/tajacks/ex-array/actions/workflows/elixir-build-and-test.yml/badge.svg)](https://github.com/tajacks/ex-array/actions/workflows/elixir-build-and-test.yml)

`ExArray.Array` is a module that provides Array-ish functionality to Elixir.

The `ExArray.Array` structure is useful when fast random access, by index, is needed. It can be dynamically grown 
and shrunk by removing or adding elements from any position in the structure. 

When passing indexes to any function in the module, negative integers are supported. Negative integers are treated as 
an offset from the end of the structure. For example, if the structure contains 5 elements, the index `-1` will return 
the last element, `-2` will return the second to last element, and so on. If the negative index runs over the beginning
of the structure, the appropriate failure condition will occur, either raises or an error tuple, depending on the 
method being called. In the example with 5 elements, the index `-6` will result in an error, as there is no element 
before the first element. When a positive index is passed, the index is treated as an offset from the beginning and 
the appropriate failure condition will occur if the index runs over the end of the structure.

Full documentation can be found at <https://hexdocs.pm/exarray>.


## Construction

`ExArray.Array` can be constructed in a number of ways. The most common is to use the `new/0` 
function to create an empty structure. Alternatively, you can use the `new/1` function to create a structure
containing the elements in the given list.

```elixir 
iex> ExArray.Array.new()
%ExArray.Array{length: 0, contents: %{}}

iex> ExArray.Array.new([1,2,3])
%ExArray.Array{length: 3, contents: %{0 => 1, 1 => 2, 2 => 3}}
```

## Accessing elements

Elements can be accessed by index using either the `get/2` or `get!/2` functions. The difference between these two 
functions is that `get!/2` will raise an error if the index is out of bounds, while `get/2` will return an error 
tuple. 

```elixir
iex> ExArray.Array.get(ExArray.Array.new([1, 2, 3]), 1)
{:ok, 2}

iex> ExArray.Array.get(ExArray.Array.new([1, 2, 3]), 3)
{:error, :out_of_bounds}

iex> ExArray.Array.get!(ExArray.Array.new([1, 2, 3]), 1)
2

iex> ExArray.Array.get!(ExArray.Array.new([1, 2, 3]), 3)
** (ArgumentError) Index 3 is out of bounds for length 3
```

## Adding Elements

Adding elements to the end of the structure is fast as no other elements need to be re-indexed. This is done using 
the `add/2` function. The `add/2` function returns a new structure with the element added to the end. 

This operation should always succeed

```elixir
iex> ExArray.Array.add(ExArray.Array.new([1, 2, 3]), 4)
%ExArray.Array{length: 4, contents: %{0 => 1, 1 => 2, 2 => 3, 3 => 4}}
```

Adding elements to the beginning or middle of the structure is slower as all elements after the index need to be 
re-indexed. This is done using `add_at/3` or `add_at!/3`. These functions return a new structure with the element added at the given index. If an element existed at the given index, it and all other elements after it will be shifted one to the right. 

```elixir 
iex> ExArray.Array.add_at(ExArray.Array.new([1, 2, 3]), 1, 4) 
{:ok, %ExArray.Array{length: 4, contents: %{0 => 1, 1 => 4, 2 => 2, 3 => 3}}}

iex> ExArray.Array.add_at(ExArray.Array.new([1, 2, 3]), 4, 4)
{:error, :out_of_bounds}

iex> ExArray.Array.add_at!(ExArray.Array.new([1, 2, 3]), 1, 4)
%ExArray.Array{length: 4, contents: %{0 => 1, 1 => 4, 2 => 2, 3 => 3}}

iex> ExArray.Array.add_at!(ExArray.Array.new([1, 2, 3]), 4, 4)
** (ArgumentError) Index 4 is out of bounds for length 3
```

## Removing Elements 

Removing elements from the end of the structure is fast as no other elements need to be re-indexed. This is done 
using the `remove/1` function. The `remove/1` function returns a new structure with the last element removed. 

If the `ExArray.Array` is empty, the `remove/1` function will return the original structure.

This operation should always succeed

```elixir 
iex> ExArray.Array.remove(ExArray.Array.new([1, 2, 3]))
%ExArray.Array{length: 2, contents: %{0 => 1, 1 => 2}}

iex> ExArray.Array.remove(ExArray.Array.new())
%ExArray.Array{length: 0, contents: %{}}
```

Removing elements from the beginning or middle of the structure is slower as all elements after the index need to be
re-indexed. This is done using `remove_at/2` or `remove_at!/2`. These functions return a new structure with the 
element at the given index removed. All elements after it will be shifted one to the left. 

```elixir
iex> ExArray.Array.remove_at(ExArray.Array.new([1, 2, 3]), 1)
{:ok, %ExArray.Array{length: 2, contents: %{0 => 1, 1 => 3}}}

iex> ExArray.Array.remove_at(ExArray.Array.new([1, 2, 3]), 4)
{:error, :out_of_bounds}

iex> ExArray.Array.remove_at!(ExArray.Array.new([1, 2, 3]), 1)
%ExArray.Array{length: 2, contents: %{0 => 1, 1 => 3}}

iex> ExArray.Array.remove_at!(ExArray.Array.new([1, 2, 3]), 4)
** (ArgumentError) Index 4 is out of bounds for length 3
```

## Replacing Elements 

An element can be replaced at the given index without requiring re-indexing. Use `set/3` and its raising 
counterpart for these operations.

```elixir 

iex> ExArray.Array.new([1, 2, 3]) |> ExArray.Array.set(1, 4)
{:ok, %ExArray.Array{length: 3, contents: %{0 => 1, 1 => 4, 2 => 3}}}

iex> ExArray.Array.new([1, 2, 3]) |> ExArray.Array.set(-1, 4)
{:ok, %ExArray.Array{length: 3, contents: %{0 => 1, 1 => 2, 2 => 4}}}

iex> ExArray.Array.new([1, 2, 3]) |> ExArray.Array.set(4, 4)
{:error, :out_of_bounds} 
```

## Collectable Operations 

`ExArray.Array` implements the `Collectable` protocol. This means that it can be used with `Enum.into` and `for` 
special forms

```elixir
iex> Enum.into([1, 2, 3], ExArray.Array.new())
%ExArray.Array{length: 3, contents: %{0 => 1, 1 => 2, 2 => 3}}

iex> for x <- [1, 2, 3], into: ExArray.Array.new(), do: x
%ExArray.Array{length: 3, contents: %{0 => 1, 1 => 2, 2 => 3}}
```

## Enum Operations

`ExArray.Array` implements the `Enumerable` protocol. This means that it can be used with the functions 
in the `Enum` module. 

```elixir 
iex> ExArray.Array.new([1, 2, 3]) |> Enum.map(&(&1 * 2))
[2, 4, 6]

iex> ExArray.Array.new([1, 2, 3]) |> Enum.reduce(&(&1 + &2))
6

iex> ExArray.Array.new([1, 2, 3]) |> Enum.filter(&(&1 > 1))
[2, 3]

iex> ExArray.Array.new([1, 2, 3]) |> Enum.at(1)
2
```

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `ex_array` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:exarray, "~> 0.2.0"}
  ]
end
```


## Acknowledgements

Special thanks to the following individuals for providing feedback on public API design:

- [@llalon](https://github.com/llalon)
- [@rgmz](https://github.com/rgmz)