Skip to content

Instantly share code, notes, and snippets.

@kottenator
Created July 13, 2015 20:44
Show Gist options
  • Save kottenator/9d936eb3e4e3c3e02598 to your computer and use it in GitHub Desktop.
Save kottenator/9d936eb3e4e3c3e02598 to your computer and use it in GitHub Desktop.
Simple pagination algorithm
// Implementation in ES6
function pagination(c, m) {
var current = c,
last = m,
delta = 2,
left = current - delta,
right = current + delta + 1,
range = [],
rangeWithDots = [],
l;
for (let i = 1; i <= last; i++) {
if (i == 1 || i == last || i >= left && i < right) {
range.push(i);
}
}
for (let i of range) {
if (l) {
if (i - l === 2) {
rangeWithDots.push(l + 1);
} else if (i - l !== 1) {
rangeWithDots.push('...');
}
}
rangeWithDots.push(i);
l = i;
}
return rangeWithDots;
}
/*
Test it:
for (let i = 1, l = 20; i <= l; i++)
console.log(`Selected page ${i}:`, pagination(i, l));
Expected output:
Selected page 1: [1, 2, 3, "...", 20]
Selected page 2: [1, 2, 3, 4, "...", 20]
Selected page 3: [1, 2, 3, 4, 5, "...", 20]
Selected page 4: [1, 2, 3, 4, 5, 6, "...", 20]
Selected page 5: [1, 2, 3, 4, 5, 6, 7, "...", 20]
Selected page 6: [1, "...", 4, 5, 6, 7, 8, "...", 20]
Selected page 7: [1, "...", 5, 6, 7, 8, 9, "...", 20]
Selected page 8: [1, "...", 6, 7, 8, 9, 10, "...", 20]
Selected page 9: [1, "...", 7, 8, 9, 10, 11, "...", 20]
Selected page 10: [1, "...", 8, 9, 10, 11, 12, "...", 20]
Selected page 11: [1, "...", 9, 10, 11, 12, 13, "...", 20]
Selected page 12: [1, "...", 10, 11, 12, 13, 14, "...", 20]
Selected page 13: [1, "...", 11, 12, 13, 14, 15, "...", 20]
Selected page 14: [1, "...", 12, 13, 14, 15, 16, "...", 20]
Selected page 15: [1, "...", 13, 14, 15, 16, 17, "...", 20]
Selected page 16: [1, "...", 14, 15, 16, 17, 18, 19, 20]
Selected page 17: [1, "...", 15, 16, 17, 18, 19, 20]
Selected page 18: [1, "...", 16, 17, 18, 19, 20]
Selected page 19: [1, "...", 17, 18, 19, 20]
Selected page 20: [1, "...", 18, 19, 20]
*/
@zacfukuda
Copy link

zacfukuda commented Jul 1, 2022

My personal implementation returns an object with current, prev, next:

function paginate({current, max}) {
  if (!current || !max) return null

  let prev = current === 1 ? null : current - 1,
      next = current === max ? null : current + 1,
      items = [1]
  
  if (current === 1 && max === 1) return {current, prev, next, items}
  if (current > 4) items.push('…')

  let r = 2, r1 = current - r, r2 = current + r

  for (let i = r1 > 2 ? r1 : 2; i <= Math.min(max, r2); i++) items.push(i)

  if (r2 + 1 < max) items.push('…')
  if (r2 < max) items.push(max)

  return {current, prev, next, items}
}
/* Test */
for (let max = 1; max < 10; max+=2) {
  console.log(`max: ${max}`)
  for (let current = 1; current <= max; current++) {
    let pagination = paginate({current, max})
    console.log(`  c:${current}`, pagination.items)
  }
}

/*
Output:
max: 1
  c:1 [1]
max: 3
  c:1 [1, 2, 3]
  c:2 [1, 2, 3]
  c:3 [1, 2, 3]
max: 5
  c:1 [1, 2, 3, '…', 5]
  c:2 [1, 2, 3, 4, 5]
  c:3 [1, 2, 3, 4, 5]
  c:4 [1, 2, 3, 4, 5]
  c:5 [1, '…', 3, 4, 5]
max: 7
  c:1 [1, 2, 3, '…', 7]
  c:2 [1, 2, 3, 4, '…', 7]
  c:3 [1, 2, 3, 4, 5, '…', 7]
  c:4 [1, 2, 3, 4, 5, 6, 7]
  c:5 [1, '…', 3, 4, 5, 6, 7]
  c:6 [1, '…', 4, 5, 6, 7]
  c:7 [1, '…', 5, 6, 7]
max: 9
  c:1 [1, 2, 3, '…', 9]
  c:2 [1, 2, 3, 4, '…', 9]
  c:3 [1, 2, 3, 4, 5, '…', 9]
  c:4 [1, 2, 3, 4, 5, 6, '…', 9]
  c:5 [1, '…', 3, 4, 5, 6, 7, '…', 9]
  c:6 [1, '…', 4, 5, 6, 7, 8, 9]
  c:7 [1, '…', 5, 6, 7, 8, 9]
  c:8 [1, '…', 6, 7, 8, 9]
  c:9 [1, '…', 7, 8, 9]
*/

@orso081980
Copy link

Is there a way to add the prev and next arrow (< >) to the original script at the very beginning and at the very end? All the solutions that I found so far is going to another direction..

@rogerfar
Copy link

Simple solution if you want the elements to always remain equal and not having your UI shift around:

  const getRange = (start: number, end: number) => {
    const length = end - start + 1;
    return Array.from({ length }, (_, i) => start + i);
  };

  const pagination = (currentPage: number, pageCount: number, delta: number) => {
    const pages: number[] = [];

    if (currentPage <= delta) {
      pages.push(...getRange(1, Math.min(pageCount, delta * 2 + 1)));
    } else if (currentPage > pageCount - delta) {
      pages.push(...getRange(Math.max(1, pageCount - delta * 2), pageCount));
    } else {
      pages.push(...getRange(Math.max(1, currentPage - delta), Math.min(pageCount, currentPage + delta)));
    }

    return pages;
  };
max: 1
   c:1 [1]
max: 3
   c:1 (3) [1, 2, 3]
   c:2 (3) [1, 2, 3]
   c:3 (3) [1, 2, 3]
max: 5
   c:1 (5) [1, 2, 3, 4, 5]
   c:2 (5) [1, 2, 3, 4, 5]
   c:3 (5) [1, 2, 3, 4, 5]
   c:4 (5) [1, 2, 3, 4, 5]
   c:5 (5) [1, 2, 3, 4, 5]
max: 7
   c:1 (7) [1, 2, 3, 4, 5, 6, 7]
   c:2 (7) [1, 2, 3, 4, 5, 6, 7]
   c:3 (7) [1, 2, 3, 4, 5, 6, 7]
   c:4 (7) [1, 2, 3, 4, 5, 6, 7]
   c:5 (7) [1, 2, 3, 4, 5, 6, 7]
   c:6 (7) [1, 2, 3, 4, 5, 6, 7]
   c:7 (7) [1, 2, 3, 4, 5, 6, 7]
max: 9
   c:1 (7) [1, 2, 3, 4, 5, 6, 7]
   c:2 (7) [1, 2, 3, 4, 5, 6, 7]
   c:3 (7) [1, 2, 3, 4, 5, 6, 7]
   c:4 (7) [1, 2, 3, 4, 5, 6, 7]
   c:5 (7) [2, 3, 4, 5, 6, 7, 8]
   c:6 (7) [3, 4, 5, 6, 7, 8, 9]
   c:7 (7) [3, 4, 5, 6, 7, 8, 9]
   c:8 (7) [3, 4, 5, 6, 7, 8, 9]
   c:9 (7) [3, 4, 5, 6, 7, 8, 9]

@Daudongit
Copy link

@narthur version in Rust
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=660c01b68363ea506b1c20fc8bdcda37

use std::collections::VecDeque;

fn pagination(current_page:i64, number_of_pages:i64)->VecDeque<String>{
    const GAP: &str = "...";
    let center = vec![
        current_page - 2, current_page - 1, current_page, current_page + 1, current_page + 2
    ];
    let mut center_deque:VecDeque<String> = center.iter()
        .filter(|&p| *p > 1i64 && *p < number_of_pages).map(i64::to_string).collect();
    let include_three_left = current_page == 5;
    let include_three_right = current_page == number_of_pages - 4;
    let include_left_dots = current_page > 5;
    let include_right_dots = current_page < number_of_pages - 4;

    if include_three_left {center_deque.push_front("2".into());}
    if include_three_right {center_deque.push_back((number_of_pages - 1i64).to_string());}
    if include_left_dots {center_deque.push_front(GAP.into());}
    if include_right_dots {center_deque.push_back(GAP.into());}
    center_deque.push_front("1".into());
    if number_of_pages > 1i64 {
        center_deque.push_back(number_of_pages.to_string());
    }
    center_deque
}

fn main(){
    let (_current_page, per_page, total) = (1u64, 20, 100);
    let mut number_of_pages = total / per_page;
    if (total % per_page) != 0u64  {
        number_of_pages += 1
    }
    _=number_of_pages; // currently not in use
    // println!("{:?}", pagination(1, number_of_pages));
    // println!("{:?}", pagination(1, 1));
    // println!("{:?}", pagination(15, 51));
    // println!("{:?}", pagination(90, 100));
    assert_eq!(pagination(1, 1), ["1"]);
    assert_eq!(pagination(15, 51), ["1", "...", "13", "14", "15", "16", "17", "...", "51"]);
    assert_eq!(pagination(90, 100), ["1", "...", "88", "89", "90", "91", "92", "...", "100"]);
}

@rrmesquita
Copy link

@narthur's approach but supporting custom delta:

function pagination(current: number, total: number, delta = 2, gap = '...') {
  if (total <= 1) return [1]

  const center = [current] as (number | typeof gap)[]

  // no longer O(1) but still very fast
  for (let i = 1; i <= delta; i++) {
    center.unshift(current - i)
    center.push(current + i)
  }

  const filteredCenter = center.filter((page) => page > 1 && page < total)

  const includeLeftGap = current > 3 + delta
  const includeLeftPages = current === 3 + delta
  const includeRightGap = current < total - (2 + delta)
  const includeRightPages = current === total - (2 + delta)

  if (includeLeftPages) filteredCenter.unshift(2)
  if (includeRightPages) filteredCenter.push(total - 1)
  if (includeLeftGap) filteredCenter.unshift(gap)
  if (includeRightGap) filteredCenter.push(gap)

  return [1, ...filteredCenter, total]
}

/*

tests:

for (let i = 1, l = 20; i <= l; i++)
  console.log(`Page ${i}:`, pagination(i, l, 4)); // delta 4

expected output:

page 1: [1, 2, 3, 4, 5, '...', 20]
page 2: [1, 2, 3, 4, 5, 6, '...', 20]
page 3: [1, 2, 3, 4, 5, 6, 7, '...', 20]
page 4: [1, 2, 3, 4, 5, 6, 7, 8, '...', 20]
page 5: [1, 2, 3, 4, 5, 6, 7, 8, 9, '...', 20]
page 6: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, '...', 20]
page 7: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, '...', 20]
page 8: [1, '...', 4, 5, 6, 7, 8, 9, 10, 11, 12, '...', 20]
page 9: [1, '...', 5, 6, 7, 8, 9, 10, 11, 12, 13, '...', 20]
page 10: [1, '...', 6, 7, 8, 9, 10, 11, 12, 13, 14, '...', 20]
page 11: [1, '...', 7, 8, 9, 10, 11, 12, 13, 14, 15, '...', 20]
page 12: [1, '...', 8, 9, 10, 11, 12, 13, 14, 15, 16, '...', 20]
page 13: [1, '...', 9, 10, 11, 12, 13, 14, 15, 16, 17, '...', 20]
page 14: [1, '...', 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
page 15: [1, '...', 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
page 16: [1, '...', 12, 13, 14, 15, 16, 17, 18, 19, 20]
page 17: [1, '...', 13, 14, 15, 16, 17, 18, 19, 20]
page 18: [1, '...', 14, 15, 16, 17, 18, 19, 20]
page 19: [1, '...', 15, 16, 17, 18, 19, 20]
page 20: [1, '...', 16, 17, 18, 19, 20]

*/

Copy link

ghost commented Oct 17, 2022

Thank you very much!

@TigersWay
Copy link

@zacfukuda Just found your implementation and loved it!

@ngoc199
Copy link

ngoc199 commented Feb 18, 2023

@narthur version is great. However, it does not catch the case when there is only one page.

If you are having that issue, add this code at the beginning of the function.

if (total === 1) {
    return [1];
}

@justingolden21
Copy link

Updated for ES6, deleted unused variables, customizable delta, first and last arrows, doesn't break if 1 page, and highlight the current page:

function pagination(current, last, delta = 2) {
  if (last === 1) return [1];

  const left = current - delta,
    right = current + delta + 1,
    range = [];

  if (last > 1 && current !== 1) {
    range.push("<");
  }

  for (let i = 1; i <= last; i++) {
    if (i == 1 || i == last || (i >= left && i < right)) {
      if (i === left && i > 2) {
        range.push("...");
      }

      if (i === current) {
        range.push("*" + i + "*");
      } else {
        range.push(i);
      }

      if (i === right - 1 && i < last - 1) {
        range.push("...");
      }
    }
  }

  if (last > 1 && current !== last) {
    range.push(">");
  }

  return range;
}

for (let i = 1, l = 20; i <= l; i++)
  console.log(`Selected page ${i}:`, pagination(i, l));

@shubhasheeshShukla
Copy link

Thanks a lot sir

@gynekolog
Copy link

Thank you for sharing. I made a few changes and checks:

// The code is based on https://gist.github.com/kottenator/9d936eb3e4e3c3e02598#gistcomment-3238804

type PageItem = number | "...";

export const getRange = (start: number, end: number): PageItem[] => {
  if (end < start) throw Error(`End number must be higher then start number: start ${start}, end ${start}`);

  const rangeLength = end - start + 1;
  return Array(rangeLength)
    .fill(0)
    .map((_, i) => i + start);
};

const clamp = (value: number, lower: number, upper: number) => Math.min(Math.max(value, lower), upper);

export const calculatePages = (currentPage: number, pageCount: number, size: number): PageItem[] => {
  if (pageCount < 1) {
    console.warn("Page count has negative value. Returning empty array.");
    return [];
  }

  if (currentPage < 1) {
    console.warn("Current page has negative value. Current page will be set to 1");
    currentPage = 1;
  }

  if (currentPage > pageCount) {
    console.warn("Current page is higher than page count. Current page will be set to page count:", pageCount);
    currentPage = pageCount;
  }

  if (size % 2 === 0) {
    console.warn("The size must be odd. The size will be increased by 1");
    size += 1;
  }

  if (size < 5) {
    console.warn("The minimum size is 5. The size will be increased to 5");
    size = 5;
  }

  const offset = (size - 1) / 2;
  const shouldAddDots = pageCount > size;

  const rangeConfig = {
    start: clamp(currentPage - offset, 1, shouldAddDots ? pageCount - size + 1 : 1),
    end: clamp(currentPage + offset, size, pageCount),
  };

  const pages = getRange(rangeConfig.start, rangeConfig.end);

  if (shouldAddDots && pages[0] !== 1) {
    pages[0] = 1;
    pages[1] = "...";
  }
  if (shouldAddDots && pages[pages.length - 1] !== pageCount) {
    pages[pages.length - 1] = pageCount;
    pages[pages.length - 2] = "...";
  }
  return pages;
};

example of the output for size 5:

[
  [1, [1, 2, 3, "...", 20]],
  [2, [1, 2, 3, "...", 20]],
  [3, [1, 2, 3, "...", 20]],
  [4, [1, "...", 4, "...", 20]],
  [5, [1, "...", 5, "...", 20]],
  [6, [1, "...", 6, "...", 20]],
  [7, [1, "...", 7, "...", 20]],
  [8, [1, "...", 8, "...", 20]],
  [9, [1, "...", 9, "...", 20]],
  [10, [1, "...", 10, "...", 20]],
  [11, [1, "...", 11, "...", 20]],
  [12, [1, "...", 12, "...", 20]],
  [13, [1, "...", 13, "...", 20]],
  [14, [1, "...", 14, "...", 20]],
  [15, [1, "...", 15, "...", 20]],
  [16, [1, "...", 16, "...", 20]],
  [17, [1, "...", 17, "...", 20]],
  [18, [1, "...", 18, 19, 20]],
  [19, [1, "...", 18, 19, 20]],
  [20, [1, "...", 18, 19, 20]],
];

The code with test is available in my gist too.

@pk936
Copy link

pk936 commented Aug 7, 2024

Another Pagination which ensures a fixed number of items in the pagination array, including ellipses as necessary same as material UI pagination.

function range(start, end) {
  return Array.from({ length: end - start + 1 }, (_, i) => i + start);
}

function pagination(currentPage, totalPages) {
  const leftSiblingIndex = Math.max(currentPage - 1, 1);
  const rightSiblingIndex = Math.min(currentPage + 1, totalPages);

  const shouldShowLeftEllipsis = leftSiblingIndex > 2;
  const shouldShowRightEllipsis = rightSiblingIndex < totalPages - 2;

  const firstPageIndex = 1;
  const lastPageIndex = totalPages;

  if (!shouldShowLeftEllipsis && shouldShowRightEllipsis) {
    const leftItemCount = 3;
    const leftRange = range(1, leftItemCount + 2);

    return [...leftRange, '...', totalPages];
  }

  if (shouldShowLeftEllipsis && !shouldShowRightEllipsis) {
    const rightItemCount = 3;
    const rightRange = range(totalPages - rightItemCount - 1, totalPages);

    return [firstPageIndex, '...', ...rightRange];
  }

  if (shouldShowLeftEllipsis && shouldShowRightEllipsis) {
    const middleRange = range(leftSiblingIndex, rightSiblingIndex);

    return [firstPageIndex, '...', ...middleRange, '...', lastPageIndex];
  }

  return range(1, totalPages);
}


// pagination(1,20) : [1, 2, 3, 4, 5, '...', 20]
// pagination(5,20) : [1, '...', 4, 5, 6, '...', 20]
// pagination(10,20) : [1, '...', 9, 10, 11, '...', 20]
// pagination(18,20) : [1, '...', 16, 17, 18, 19, 20]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment