I wrote this Paginator module that's part of a small application that displays Elixir Github repos in a table.
I'm new to Elixir and looking for code improvements with as much as possible. Best practices. Commenting. Optimizations. The usage of structs for input output. Etc.
paginator.ex
defmodule Paginator do
@moduledoc """
A module that implements functions for performing simple pagination functionality
Invoke it using the module's `call` function that takes a struct as parameter.
## Struct key values
- total: The total amount of records (mandatory)
- page: The page currently on (default: 1)
- per_page: The number of records per page (default: 10)
- max_display: The number of pages displayed in the pagination menu (default: 10)
- max_results: Optional argument that limits the total number of records it can paginate
## Examples
iex> Paginator.call %Paginator(total: 1000)
%Paginator.Output{
first: nil,
last: 100,
next: 2,
page: 1,
pages: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
previous: nil
}
"""
# The struct with the pagination info that gets returned
defmodule Output do
defstruct [:page, :first, :last, :previous, :next, :pages]
end
@doc """
# Struct that's passed to the module used to calculate the pagination data
"""
@enforce_keys [:total]
defstruct page: 1, per_page: 10, total: nil, max_display: 10, max_results: nil
@doc """
Invokes the module. Takes a struct with the input and returns a struct with the pagination data
"""
def call(data) do
data = data
|> Map.put(:max_pages, max_pages(data))
|> Map.put(:half, half(data))
%Output{
page: data.page,
first: first(data),
last: last(data),
pages: pages(data),
previous: previous(data),
next: next(data)
}
end
# Returns the maximum pages.
defp max_pages(data) do
cond do
data.total <= data.per_page ->
0
data.max_results !== nil and data.max_results < data.total ->
Kernel.trunc(data.max_results / data.per_page)
true ->
Kernel.trunc(data.total / data.per_page)
end
end
# Returns the first page
defp first(data) do
if data.total >= data.per_page and data.page !== 1, do: 1, else: nil
end
# Returns the last page
defp last(data) do
if data.page < data.max_pages, do: data.max_pages, else: nil
end
# Returns the half value of `max_display` rounded down
defp half(data) do
Kernel.trunc(data.max_display / 2)
end
# Returns the `pages` list. The number of list items depends on the number specified in `max_display`
defp pages(data) do
if data.total === nil or data.total === 0 or data.max_pages === 0 do
[]
else
Enum.to_list begin_pages(data)..end_pages(data)
end
end
# Returns the page that the `pages` list starts on
defp begin_pages(data) do
cond do
data.page + data.half >= data.max_pages ->
# when reaching the end
data.max_pages - (data.max_display - 1)
data.page > data.half ->
# odd vs even pages
if rem(data.max_display, 2) === 0 do
data.page - (data.half - 1)
else
data.page - data.half
end
true ->
1
end
end
# Returns the page that the `pages` list ends on
defp end_pages(data) do
end_page = data.page + data.half
cond do
end_page >= data.max_pages ->
# when reaching the end
data.max_pages
data.page <= data.half ->
data.max_display
true ->
end_page
end
end
# Returns the page number that is prior than the current page.
# If the current page is 1 it returns nil
defp previous(data) do
if data.page > 1, do: data.page - 1, else: nil
end
# Returns the page number that is latter than the current page.
# If the current page is equal to the last it returns nil
defp next(data) do
if data.page < data.max_pages, do: data.page + 1, else: nil
end
end
paginator_test.exs
defmodule PaginatorTest do
use ConnCase
alias Paginator
describe "Test Paginator" do
test "page 1 of 15" do
res = Paginator.call %Paginator{total: 150}
assert res.first == nil
assert res.previous == nil
assert res.next == 2
assert res.last == 15
assert res.pages == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
end
test "page 5 of 15" do
res = Paginator.call %Paginator{page: 5, total: 150}
assert res.first == 1
assert res.previous == 4
assert res.next == 6
assert res.last == 15
assert res.pages == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
end
test "page 6 of 15" do
res = Paginator.call %Paginator{page: 6, total: 150}
assert res.first == 1
assert res.previous == 5
assert res.next == 7
assert res.last == 15
assert res.pages == [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
end
test "page 7 of 15" do
res = Paginator.call %Paginator{page: 7, total: 150}
assert res.pages == [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
end
test "page 13 of 15" do
res = Paginator.call %Paginator{page: 13, total: 150}
assert res.pages == [6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
end
test "per page 50" do
res = Paginator.call %Paginator{page: 25, per_page: 50, total: 2000}
assert res.first == 1
assert res.previous == 24
assert res.next == 26
assert res.last == 40
assert res.pages == [21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
end
test "last page" do
res = Paginator.call %Paginator{page: 50, total: 500}
assert res.first == 1
assert res.previous == 49
assert res.next == nil
assert res.last == nil
end
test "max display" do
res = Paginator.call %Paginator{page: 8, max_display: 5, total: 2000}
assert res.first == 1
assert res.previous == 7
assert res.next == 9
assert res.pages == [6, 7, 8, 9, 10]
res = Paginator.call %Paginator{page: 9, max_display: 5, total: 2000}
assert res.pages == [7, 8, 9, 10, 11]
end
test "max results - total more than max" do
res = Paginator.call %Paginator{page: 96, total: 2000, max_results: 1000}
assert res.last == 100
assert res.pages == [91, 92, 93, 94, 95, 96, 97, 98, 99, 100]
end
test "max results - max more than total" do
res = Paginator.call %Paginator{page: 96, total: 2000, max_results: 1000}
assert res.last == 100
assert res.pages == [91, 92, 93, 94, 95, 96, 97, 98, 99, 100]
end
test "no pages - zero total" do
res = Paginator.call %Paginator{total: 0}
assert res.first == nil
assert res.previous == nil
assert res.next == nil
assert res.pages == []
end
test "no pages - low total" do
res = Paginator.call %Paginator{total: 5}
assert res.first == nil
assert res.previous == nil
assert res.next == nil
assert res.pages == []
end
end
end