1:- encoding(utf8).
    2:- module(
    3  pagination,
    4  [
    5    pagination/3,           % +Template, :Goal_0, -Page
    6    pagination/4,           % +Template, :Goal_0, +Options, -Page
    7    pagination/5,           % +Template, :Goal_0, :Estimate_1, +Options, -Page
    8    pagination_bulk/3,      % :Goal_1, +Options, -Page
    9    pagination_bulk/4,      % +Template, :Goal_0, +Options, -Page
   10    pagination_is_at_end/1, % +Result
   11    pagination_is_empty/1,  % +Result
   12    pagination_page/3,      % +Page, ?Relation, -PageNumber
   13    pagination_range/2      % +Page, -Range
   14  ]
   15).

Pagination support

This module creates pages that group results.

?- pagination(N, between(1, 1000000, N), options{page: 856}, Result).
Result = _G120{number_of_results:20, page:856, page_size:20, results:[17101, 17102, 17103, 17104, 17105, 17106, 17107, 17108|...]} ;
Result = _G147{number_of_results:20, page:857, page_size:20, results:[17121, 17122, 17123, 17124, 17125, 17126, 17127, 17128|...]} ;
Result = _G147{number_of_results:20, page:858, page_size:20, results:[17141, 17142, 17143, 17144, 17145, 17146, 17147, 17148|...]}

*/

   30:- use_module(library(aggregate)).   31:- use_module(library(error)).   32:- use_module(library(settings)).   33
   34:- use_module(library(dict)).   35:- use_module(library(list_ext)).   36
   37:- meta_predicate
   38    pagination(?, 0, -),
   39    pagination(?, 0, +, -),
   40    pagination(?, 0, 1, +, -),
   41    pagination_bulk(1, +, -),
   42    pagination_bulk(?, 0, +, -).   43
   44:- setting(default_page_size, positive_integer, 10,
   45           "The default number of triples that is retreived in one request.").   46:- setting(maximum_page_size, positive_integer, 100,
   47           "The maximum number of triples that can be retrieved in one request.").
 empty_pagination(+Options:options, -Page:dict) is det
Returns a pagination Page dictionary with empty results.
   57empty_pagination(Options, Page) :-
   58  pagination(_, fail, Options, Page).
 pagination(+Templ, :Goal_0, -Page:dict) is det
 pagination(+Templ, :Goal_0, +Options:options, -Page:dict) is det
 pagination(+Templ, :Goal_0, :Estimate_1, +Options:options, -Page:dict) is det
Arguments:
Options- The following options are supported:
page_number(+nonneg)
The page number of the Page in the implicit sequence. The default is 1.
page_size(+positive_integer)
The number of results per (full) page. The default is 10.
Page- is a dictionary with the following keys:
number_of_results(nonneg)
The number of results on the page. This is either identical to or less than the value of option page_size/1.
page_number(positive_integer)
The page number of the page within the implicit sequence.
page_size(positive_integer)
The number of results if the page were full. This is identical to the value of option page_size/1.
results(list(term))
The results that are held by this Page.
single_page(boolean)
A dirty hack to allow ‘pagination’ of one page, i.e., without ‘next’ links.
total_number_of_results(nonneg)
The total number of results, independent of pagination. This is only present when etimation goal Estimate_1 is passed.
  108pagination(Templ, Goal_0, Page) :-
  109  pagination(Templ, Goal_0, options{}, Page).
  110
  111
  112pagination(Templ, Goal_0, Options1, Page2) :-
  113  pagination_options(Options1, PageNumber, PageSize, Options2),
  114  Offset is PageSize * (PageNumber - 1),
  115  findall(Templ, limit(PageSize, offset(Offset, Goal_0)), Results),
  116  length(Results, NumResults),
  117  Page1 = options{
  118    number_of_results: NumResults,
  119    page_number: PageNumber,
  120    page_size: PageSize,
  121    results: Results,
  122    single_page: false
  123  },
  124  merge_dicts(Options2, Page1, Page2).
  125pagination(_, _, Options, Page) :-
  126  empty_pagination(Options, Page).
  127
  128
  129pagination(Templ, Goal_0, Estimate_1, Options1, Page2) :-
  130  pagination(Templ, Goal_0, Options1, Page1),
  131  call(Estimate_1, TotalNumResults),
  132  dict_put(total_number_of_results, Page1, TotalNumResults, Page2).
 pagination_bulk(:Goal_1, +Options:options, -Page:dict) is nondet
 pagination_bulk(?Template, :Goal_0, +Options:options, -Page:dict) is nondet
  139pagination_bulk(Goal_1, Options, Page) :-
  140  call(Goal_1, AllResults),
  141  pagination_bulk_(AllResults, Options, Page).
  142
  143
  144pagination_bulk(Templ, Goal_0, Options, Page) :-
  145  aggregate_all(set(Templ), Goal_0, AllResults),
  146  pagination_bulk_(AllResults, Options, Page).
  147
  148pagination_bulk_(AllResults, Options1, Page2) :-
  149  pagination_options(Options1, StartPageNumber, PageSize, Options2),
  150  length(AllResults, TotalNumberOfResults),
  151  NumberOfPages is max(1, ceil(TotalNumberOfResults / PageSize)),
  152  must_be(between(1, NumberOfPages), StartPageNumber),
  153  between(StartPageNumber, NumberOfPages, PageNumber),
  154  SkipLength is PageSize * (PageNumber - 1),
  155  length(Skip, SkipLength),
  156  append(Skip, Rest, AllResults),
  157  list_truncate(Rest, PageSize, Results),
  158  length(Results, NumberOfResults),
  159  Page1 = options{
  160    number_of_results: NumberOfResults,
  161    page_number: PageNumber,
  162    page_size: PageSize,
  163    results: Results,
  164    single_page: false,
  165    total_number_of_results: TotalNumberOfResults
  166  },
  167  merge_dicts(Options2, Page1, Page2).
 pagination_is_at_end(+Page:dict) is semidet
Succeeds if Page is the last page.

Since we do not know the total number of results, the last page may be empty.

  178pagination_is_at_end(Page) :-
  179  options{single_page: true} :< Page, !.
  180pagination_is_at_end(Page) :-
  181  dict_get(total_number_of_results, Page, TotalNumResults),
  182  Page.page_number >= ceil(TotalNumResults / Page.page_size), !.
  183pagination_is_at_end(Page) :-
  184  Page.number_of_results < Page.page_size.
 pagination_is_empty(+Page:dict) is semidet
  190pagination_is_empty(Page) :-
  191  Page.number_of_results =:= 0.
 pagination_options(+Options1:dict, -StartPageNumber:positive_integer, -PageSize:nonneg, -Options2:dict) is det
  200pagination_options(Options1, StartPageNumber, PageSize, Options3) :-
  201  dict_delete(page_number, Options1, 1, StartPageNumber, Options2),
  202  (   dict_delete(page_size, Options2, PageSize, Options3)
  203  ->  true
  204  ;   setting(default_page_size, PageSize),
  205      Options3 = Options2
  206  ).
 pagination_page(+Page:dict, +Relation:atom, -PageNumber:positive_integer) is semidet
pagination_page(+Page:dict, -Relation:atom, -PageNumber:positive_integer) is nondet
PageNumber is the first, last, next, and previous page number for Page.

Fails silently when there is no page with relation Relation.

Arguments:
Relation- is either `first', `last', `next', or `prev'.
PageNumber- is a positive integer.
  222pagination_page(Page, first, 1) :-
  223  Page.number_of_results > 0.
  224pagination_page(Page, last, PageNumber) :-
  225  dict_get(total_number_of_results, Page, TotalNumResults),
  226  PageNumber is ceil(TotalNumResults / Page.page_size).
  227pagination_page(Page, next, PageNumber) :-
  228  \+ pagination_is_at_end(Page),
  229  PageNumber is Page.page_number + 1.
  230pagination_page(Page, prev, PageNumber) :-
  231  PageNumber is Page.page_number - 1,
  232  PageNumber > 0.
 pagination_range(+Page:dict, -Range:pair(nonneg)) is det
Returns the range of results that is spanned by the given Page.
  240pagination_range(Page, 0-0) :-
  241  Page.number_of_results =:= 0, !.
  242pagination_range(Page, Low-High) :-
  243  Low is (Page.page_number - 1) * Page.page_size + 1,
  244  High is Low + Page.number_of_results - 1