impl/typescript.ts
2,415 bytes · the TypeScript implementation · view raw
import { type Page } from "./collections_paginate_types.ts";
/**
* Take the 1-based page `page` of `items`, `perPage` items at a time.
*
* Page numbers are 1-based because every pagination UI, query string and API
* contract in the wild is 1-based; page 0 can only come from a caller that has
* the contract wrong, so it raises.
*
* A page past the end is deliberately not an error. Between two requests a
* collection can shrink under a client that is already on page 9, and that
* client should render an empty page, not a 500.
*/
export function paginate<T>(items: readonly T[], page: number, perPage: number): Page<T> {
if (!Array.isArray(items)) {
throw new TypeError("paginate needs a list of items");
}
if (!Number.isInteger(page)) {
throw new TypeError(`page must be a whole number, received ${page}`);
}
if (!Number.isInteger(perPage)) {
throw new TypeError(`perPage must be a whole number, received ${perPage}`);
}
// A page size of zero cannot mean "no limit": that would quietly hand back
// the entire collection, which is the outage pagination exists to prevent.
if (perPage < 1) {
throw new RangeError(`perPage must be at least 1, received ${perPage}`);
}
if (page < 1) {
throw new RangeError(`page must be 1 or greater, received ${page}`);
}
const total = items.length;
// Zero items is zero pages, not one empty page, so `page <= totalPages` is
// the one correct test for "does this page exist".
const totalPages = Math.ceil(total / perPage);
const start = (page - 1) * perPage;
return {
// slice() already clamps, so a start past the end yields [] rather than
// throwing - the documented behaviour for a page past the end.
items: items.slice(start, start + perPage) as T[],
page,
perPage,
total,
totalPages,
hasNext: page < totalPages,
// On page 9 of a 2-page collection there really are earlier pages.
hasPrev: page > 1,
};
}
/** The 1-based page number a zero-based item index falls on. */
export function pageOfIndex(index: number, perPage: number): number {
if (!Number.isInteger(index) || index < 0) {
throw new RangeError(`index must be a whole number of 0 or greater, received ${index}`);
}
if (!Number.isInteger(perPage) || perPage < 1) {
throw new RangeError(`perPage must be at least 1, received ${perPage}`);
}
return Math.floor(index / perPage) + 1;
}