Functional Weave
Code in Python

collections.paginate@1.0.0

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;
}