Skip to content

Instantly share code, notes, and snippets.

@kottenator
Created July 13, 2015 20:44
Star You must be signed in to star a gist
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]
*/
@iiceman40
Copy link

@jorrit91 very nice, works like a charm and feels pretty good to always have the same number of UI elements, indeed.

@ktmud
Copy link

ktmud commented Jun 3, 2020

My simple implementation in TypeScript and Bootstrap:

// first, ..., prev, current, next, ..., last
const MINIMAL_PAGE_ITEM_COUNT = 7;

/**
 * Generate numeric page items around current page.
 *   - Always include first and last page
 *   - Add ellipsis if needed
 */
function generatePageItems(total: number, current: number, width: number) {
  if (width < MINIMAL_PAGE_ITEM_COUNT) {
    throw new Error(`Must allow at least ${MINIMAL_PAGE_ITEM_COUNT} page items`);
  }
  if (width % 2 === 0) {
    throw new Error(`Must allow odd number of page items`);
  }
  if (total < width) {
    return [...new Array(total).keys()];
  }
  const left = Math.max(0, Math.min(total - width, current - Math.floor(width / 2)));
  const items: (string | number)[] = new Array(width);
  for (let i = 0; i < width; i += 1) {
    items[i] = i + left;
  }
  // replace non-ending items with placeholders
  if (items[0] > 0) {
    items[0] = 0;
    items[1] = 'prev-more';
  }
  if (items[items.length - 1] < total - 1) {
    items[items.length - 1] = total - 1;
    items[items.length - 2] = 'next-more';
  }
  return items;
}
interface PaginationProps {
  pageCount: number; // number of pages
  currentPage?: number; // index of current page, zero-based
  maxPageItemCount?: number;
  ellipsis?: string; // content for ellipsis item
  gotoPage: (page: number) => void; // `page` is zero-based
}

export default React.forwardRef(function Pagination(
  { pageCount, currentPage = 0, maxPageItemCount = 9, gotoPage }: PaginationProps,
  ref: React.Ref<HTMLDivElement>,
) {
  const pageItems = generatePageItems(pageCount, currentPage, maxPageItemCount);
  return (
    <div ref={ref} className="dt-pagination">
      <ul className="pagination pagination-sm">
        {pageItems.map((item, i) =>
          typeof item === 'number' ? (
            // actual page number
            <li key={item} className={currentPage === item ? 'active' : undefined}>
              <a
                href={`#page-${item}`}
                role="button"
                onClick={e => {
                  e.preventDefault();
                  gotoPage(item);
                }}
              >
                {item + 1}
              </a>
            </li>
          ) : (
            <li key={item} className="dt-pagination-ellipsis">
              <span></span>
            </li>
          ),
        )}
      </ul>
    </div>
  );
});

@edgarjaviertec
Copy link

@ktmud Bro your implementation helped me a lot, thank you very much for sharing it 👍

@marcofbb
Copy link

great !

@narthur
Copy link

narthur commented Aug 11, 2020

Version that is just as fast regardless of number of pages:

export function pagination(current, total) {
    const center = [current - 2, current - 1, current, current + 1, current + 2],
        filteredCenter = center.filter((p) => p > 1 && p < total),
        includeThreeLeft = current === 5,
        includeThreeRight = current === total - 4,
        includeLeftDots = current > 5,
        includeRightDots = current < total - 4;

    if (includeThreeLeft) filteredCenter.unshift(2)
    if (includeThreeRight) filteredCenter.push(total - 1)

    if (includeLeftDots) filteredCenter.unshift('...');
    if (includeRightDots) filteredCenter.push('...');

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

Tests:

describe("pagination algorithm", () => {
    // https://gist.github.com/kottenator/9d936eb3e4e3c3e02598
    const runner = test.each([
        [1,[1, 2, 3, "...", 20]],
        [2,[1, 2, 3, 4, "...", 20]],
        [3,[1, 2, 3, 4, 5, "...", 20]],
        [4,[1, 2, 3, 4, 5, 6, "...", 20]],
        [5,[1, 2, 3, 4, 5, 6, 7, "...", 20]],
        [6,[1, "...", 4, 5, 6, 7, 8, "...", 20]],
        [7,[1, "...", 5, 6, 7, 8, 9, "...", 20]],
        [8,[1, "...", 6, 7, 8, 9, 10, "...", 20]],
        [9,[1, "...", 7, 8, 9, 10, 11, "...", 20]],
        [10,[1, "...", 8, 9, 10, 11, 12, "...", 20]],
        [11,[1, "...", 9, 10, 11, 12, 13, "...", 20]],
        [12,[1, "...", 10, 11, 12, 13, 14, "...", 20]],
        [13,[1, "...", 11, 12, 13, 14, 15, "...", 20]],
        [14,[1, "...", 12, 13, 14, 15, 16, "...", 20]],
        [15,[1, "...", 13, 14, 15, 16, 17, "...", 20]],
        [16,[1, "...", 14, 15, 16, 17, 18, 19, 20]],
        [17,[1, "...", 15, 16, 17, 18, 19, 20]],
        [18,[1, "...", 16, 17, 18, 19, 20]],
        [19,[1, "...", 17, 18, 19, 20]],
        [20,[1, "...", 18, 19, 20]],
    ])

    runner('pagination(%i, 20)', (index, expected) => {
        expect(pagination(index, 20)).toStrictEqual(expected)
    })

    it("maintains performance", () => {
        const t0 = performance.now()
        pagination(1, 99999999999)
        const t1 = performance.now()

        expect(t1 - t0).toBeLessThan(1)
    })
})

@mavyfaby
Copy link

Thanks bro!

@hazmihaz
Copy link

hazmihaz commented Nov 18, 2020

@narthur I like your implementation. Thanks for sharing!

edit: I found a bug
If total page is 1, it returns [1,1] instead of [1].

So I added simple checking at line 2:
if (total <= 1) return [1]

@eliot-akira
Copy link

@jorrit91 Thanks for sharing your implementation, brilliant.

Of all the variations in this thread, yours made the most sense for my use case; I believe it provides the best UX with a constant number of visible page elements.

@RoLYroLLs
Copy link

@jorrit91 Love your version and was exactly what I'm looking for, but I need it for C#. I'll be trying to convert it, unless someone gets in before me.

@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

Thanks dude, you saved my day...

@taythebot
Copy link

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

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

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.

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