Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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]
*/
@RoLYroLLs
Copy link

RoLYroLLs commented Dec 8, 2020

Here's my C# version @jorrit91. I'm sure some of you can optimize it, so let me know if you do.. Thanks.

https://gist.github.com/RoLYroLLs/c165202f72a256938da15c916b1362b8

or

public IEnumerable<object> Pages(int current, int pageCount) {
    List<object> pages = new List<object>();
    var delta = 7;

    if (pageCount > 7) {
            delta = current > 4 && current < pageCount - 3 ? 2 : 4;
    }

    var startIndex = (int)Math.Round(current - delta / (double)2);
    var endIndex = (int)Math.Round(current + delta / (double)2);

    if (startIndex - 1 == 1 || endIndex + 1 == pageCount) {
            startIndex += 1;
            endIndex += 1;
    }

    var to = Math.Min(pageCount, delta + 1);
    for (int i = 1; i <= to; i++) {
            pages.Add(i);
    }

    if (current > delta) {
            pages.Clear();
            var from = Math.Min(startIndex, pageCount - delta);
            to = Math.Min(endIndex, pageCount);
            for (int i = from; i <= to; i++) {
                    pages.Add(i);
            }
    }

        if (pages[0].ToString() != "1") {
                if (pages.Count() + 1 != pageCount) {
                        pages.Insert(0, "...");
                }
                pages.Insert(0, 1);
        }

        if ((int)pages.Last() < pageCount) {
                if (pages.Count() + 1 != pageCount) {
                        pages.Add("...");
                }
                pages.Add(pageCount);
        }

    return pages;
}

@danielkochdakitec
Copy link

danielkochdakitec commented Mar 22, 2021

Thank you very much! In case someone needs this in PHP, I took your example and moved it to PHP:

<?php
  $currentPage = 10;
  $length = 20;
  
  $delta = 2;
  $left = $currentPage - $delta;
  $right = $currentPage + $delta + 1;
  $range = [];
  $rangeWithDots = [];
  $l;

  for($i = 1; $i <= $length; $i++) {
    if($i == 1 || $i == $length || $i >= $left && $i < $right) {
      $range[] = $i;
    }
  }

  foreach($range as $i) {
    if($l) {
      if ($i - $l === 2) {
        $rangeWithDots[] = $l + 1;
      } else if ($i - $l !== 1) {
        $rangeWithDots[] = '...';
      }
    }
    
    $rangeWithDots[] = $i;
    $l = $i;
  }
  
  print_r($rangeWithDots);

@chrisk8er
Copy link

chrisk8er commented Jun 23, 2021

Thanks dude, you saved my day...

@taythebot
Copy link

taythebot commented Jul 17, 2021

Thank you so much for this. Now I don't have to rip out my hair figuring this out..

@robozb
Copy link

robozb commented Aug 25, 2021

Thank you so much!

@YuraKostin
Copy link

YuraKostin commented Aug 25, 2021

Hello, everyone

Here is my view of paginator implementation

type PaginatorInput = {
    current: number;
    last: number;
    betweenFirstAndLast?: number;
};

type Paginator = {
    first: number;
    current: number;
    last: number;
    pages: Array<number>;
    leftCluster: number | null;
    rightCluster: number | null;
};

const thresholds = (current: number, side: number): [number, number] => [
    current - side,
    current + side,
];

const range = (from: number, to: number) =>
    Array.from({ length: to + 1 - from }).map((_, i) => i + from);

const middle = (n: number): number => Math.floor(n / 2);

const paginator = (options: PaginatorInput): Paginator | null => {
    const base = {
        first: 1,
        last: options.last,
        current: options.current,
    };

    const { betweenFirstAndLast = 7 } = options;

    if (options.last <= betweenFirstAndLast + 2) {
        return {
            pages: range(2, options.last - 1),
            leftCluster: null,
            rightCluster: null,
            ...base,
        };
    }

    const side = middle(betweenFirstAndLast);
    const [left, right] = thresholds(options.current, side);

    if (left > 1 && right < options.last) {
        return {
            pages: range(left, right),
            leftCluster: middle(1 + left),
            rightCluster: middle(right + options.last),
            ...base,
        };
    }

    if (left < 1) {
        return {
            pages: range(2, right + side),
            leftCluster: null,
            rightCluster: middle(right + options.last),
            ...base,
        };
    }

    if (right > options.last) {
        return {
            pages: range(left - side, options.last - 1),
            leftCluster: middle(right + options.last),
            rightCluster: null,
            ...base,
        };
    }

    return null;
};

And here is the example of render function

const paginatorList = (p: Paginator) => {
    if (!p) {
        return [];
    };

    const {first, current, last, pages, leftCluster, rightCluster} = p;
    const list = [];

    if (current !== first) {
        list.push(first);
    } else {
        list.push('[' + first +']')
    }

    if (leftCluster) {
        list.push('...')
    }

    pages.forEach(page => {
        if (page === current) {
            list.push('[' + page +']');
        } else {
            list.push(page);
        }
    });

    if (rightCluster) {
        list.push('...')
    }

    if (current !== last) {
        list.push(last);
    } else {
        list.push('[' + last +']')
    }

    return list;
};

@roblevintennis
Copy link

roblevintennis commented Dec 27, 2021

I really liked the currying approach that @narthur used. It's so straight forward and readable and removes the need for looping! I thought about this and realized even a flexible API would only need to offer offset of say 1 | 2 and so I adapted his to support both. Here's the React hook version (I plan to also implement this in Vue 3, Svelte, and Angular):

export type allowedOffsets = 1 | 2;
export type GAP = '...';
export type PageArrayItem = number | GAP;

export interface PaginationProps {
  offset?: allowedOffsets;
  onChange?: (page: number, pages: PageArrayItem[]) => void;
}

export const usePagination = ({ offset = 2 }: PaginationProps) => {
  const getPaddedArray = (
    filtered: PageArrayItem[],
    shouldIncludeLeftDots: boolean,
    shouldIncludeRightDots: boolean,
    totalCount: number,
  ) => {
    if (shouldIncludeLeftDots) {
      filtered.unshift('...');
    }
    if (shouldIncludeRightDots) {
      filtered.push('...');
    }
    if (totalCount <= 1) {
      return [1];
    }
    return [1, ...filtered, totalCount];
  };

  const generatePagingPaddedByOne = (current: number, totalPageCount: number) => {
    const center = [current - 1, current, current + 1];
    const filteredCenter: PageArrayItem[] = center.filter((p) => p > 1 && p < totalPageCount);
    const includeLeftDots = current > 3;
    const includeRightDots = current < totalPageCount - 2;
    return getPaddedArray(filteredCenter, includeLeftDots, includeRightDots, totalPageCount);
  };

  const generatePagingPaddedByTwo = (current: number, totalPageCount: number) => {
    const center = [current - 2, current - 1, current, current + 1, current + 2];
    const filteredCenter: PageArrayItem[] = center.filter((p) => p > 1 && p < totalPageCount);
    const includeThreeLeft = current === 5;
    const includeThreeRight = current === totalPageCount - 4;
    const includeLeftDots = current > 5;
    const includeRightDots = current < totalPageCount - 4;

    if (includeThreeLeft) {
      filteredCenter.unshift(2);
    }
    if (includeThreeRight) {
      filteredCenter.push(totalPageCount - 1);
    }

    return getPaddedArray(filteredCenter, includeLeftDots, includeRightDots, totalPageCount);
  };

  // https://gist.github.com/kottenator/9d936eb3e4e3c3e02598#gistcomment-3413141
  const generate = (current: number, totalPageCount: number): PageArrayItem[] => {
    if (offset === 1) {
      const generatedPages = generatePagingPaddedByOne(current, totalPageCount);
      return generatedPages;
    }
    const generatedPages = generatePagingPaddedByTwo(current, totalPageCount);
    return generatedPages;
  };

  return {
    generate,
  };
};

Tests are here. This is what it looks like used in my pagination component when the offset is set to 2 (I've left the focus ring as it supports keyboard navigation via tabbing):

Screen Shot 2021-12-26 at 11 24 22 PM

I did notice Ant Design and Zendesk Garden ones used padding (offset, sibling) of just 1 on each side so it's probably worth supporting as well.

I do like the implementations I've seen here that keep the number of page links constant as they don't "jump around" but I think it's a bit of a trade off.

@farnaz-kakhsaz
Copy link

farnaz-kakhsaz commented Jun 3, 2022

I hope this will help:

export const getPaginationGenerator = (
  currentPageNumber: number,
  totalPageNumber: number,
  offset = 2
): number[] | string[] => {
  // By doing this, when we are close to the beginning or end of the pagination, two numbers are generated after/before the current page, 
  // but when we are far from these points (in the middle of the pagination), we generate only one number after/before the current page.
  const offsetNumber =
    currentPageNumber <= offset || currentPageNumber > totalPageNumber - offset ? offset : offset - 1;
  const numbersList = [];
  const numbersListWithDots = [];

  // If itemsPerPage is less than what the user selected with the Select component or if there is no page or only one page:
  if (totalPageNumber <= 1 || totalPageNumber === undefined) return [1];

  // Create list of numbers:
  numbersList.push(1);
  for (let i = currentPageNumber - offsetNumber; i <= currentPageNumber + offsetNumber; i++) {
    if (i < totalPageNumber && i > 1) {
      numbersList.push(i);
    }
  }
  numbersList.push(totalPageNumber);

  // Add three dots to the list of numbers:
  numbersList.reduce((accumulator, currentValue) => {
    if (accumulator === 1) {
      numbersListWithDots.push(accumulator);
    }
    if (currentValue - accumulator !== 1) {
      numbersListWithDots.push('...');
    }
    numbersListWithDots.push(currentValue);

    return currentValue;
  });

  return numbersListWithDots;
};

image

@ponnex
Copy link

ponnex commented Jun 29, 2022

@artanik @jorrit91

I've slightly altered the version of your codes by adding pagesShown to have a variable fixed length. pagesShown is clamped to 5 as it makes no sense to have a [1 ... ] or [1 ... ... 10] scenario in pages. With this, we could set whichever fixed length we want.

pagination

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

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

const pagination = (
    currentPage: number,
    pageCount: number,
    pagesShown: number,
    MINIMUM_PAGE_SIZE: number = 5,
) => {
    let delta: number;
    currentPage = clamp(currentPage, 1, pageCount);
    pagesShown = clamp(pagesShown, MINIMUM_PAGE_SIZE, pageCount);
    const centerPagesShown = pagesShown - 5;
    const boundaryPagesShown = pagesShown - 3;

    if (pageCount <= pagesShown) {
        delta = pagesShown;
    } else {
        delta =
            currentPage < boundaryPagesShown || currentPage > pageCount - boundaryPagesShown
                ? boundaryPagesShown
                : centerPagesShown;
    }

    const range = {
        start: Math.round(currentPage - delta / 2),
        end: Math.round(currentPage + delta / 2),
    };

    if (range.start - 1 === 1 || range.end + 1 === pageCount) {
        range.start += 1;
        range.end += 1;
    }
    let pages: (string | number)[] =
        currentPage > delta
            ? getRange(Math.min(range.start, pageCount - delta), Math.min(range.end, pageCount))
            : getRange(1, Math.min(pageCount, delta + 1));

    if (currentPage > pageCount - boundaryPagesShown && pageCount > pagesShown) {
        pages = getRange(pageCount - delta, pageCount);
    }

    const withDots = (value: number, pair: (string | number)[]) =>
        pages.length + 1 !== pageCount ? pair : [value];
    const lastPage = pages[pages.length - 1];

    if (pages[0] !== 1) {
        pages = withDots(1, [
            1,
            '...',
        ]).concat(pages);
    }

    if (lastPage && lastPage < pageCount) {
        pages = pages.concat(
            withDots(pageCount, [
                '...',
                pageCount,
            ]),
        );
    }

    return pages;
};

@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

orso081980 commented Jul 7, 2022

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

rogerfar commented Sep 19, 2022

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

Daudongit commented Sep 25, 2022

@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

rrmesquita commented Sep 30, 2022

@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]

*/

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