3
\$\begingroup\$

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
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.