#StackBounty: #unit-testing #pagination #elixir Paginator module in Elixir

Bounty: 100

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


Get this bounty!!!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.