ページネーション実装してみたメモ

特定の言語に依存しないようにアルゴリズムというか実装方法的なメモです。
ただ実際のコードはPHPで書いてます。

まず先にこちらをご覧ください。

https://kurage-worker.com/2022/pagination-2

その続きです。

リスト系のページを作成するとしましょう。

アイテムの数がめっちゃ多いと(例えば1000件)、全部表示するのは不便なので数ページに分けて表示します。
ただページ数もめっちゃ多い(例えば1ページ10件表示だと100ページ)と、それまた大量のリンク張ると不便です。

そこでシンプルなコードでページネーションを実装してみました。

現在表示中のページは( )でくくる、最初のページ、最後のページは[ ]でくくる、という条件を先に決めておきます。

100ページあり、現在50ページ目を表示しているとします。

[1].....(50).....[100]

というナビを作成することにしましょう。
これだけではさみしいので、現在表示中のページの前後に3ページ分追加するとしましょう。

[1]..... 47 48 49 (50) 51 52 53 .....[100]

さらに最初のページ、最後のページの付近に2ページ分追加することにしましょう。

[1] 2 3 ..... 47 48 49 (50) 51 52 53 ..... 98 99 [100]

これでだいぶ豪華なナビになりました。

ではこれをどうやって実装するか、出来るだけ簡単なアルゴリズムを考えます。

最初のページ

最初のページは1なので、(1, 1 + 2) = (1, 3) までの範囲です。
現在のページは50なので、(50 - 3, 50 + 3) = (47, 53) までの範囲です。
最後のページは100なので、(100 - 2, 100) = (98, 100) までの範囲です。

この全範囲を配列に追加していきます。

[1, 2, 3, 47, 48, 49, 50, 51, 52, 53, 98, 99, 100] と小さい順で追加していきます。

ただし重複は排除するように作成します。
表示するページ塗りつぶすという表現をしようと思います。

この例だと3と57, 53と98の間に空白が存在します。
これはn番目からn-1番目の値を引いて、値が1を超えれば空白があるの判断しましょう。
たとえば、(47 - 3)は1を超えます。(98 - 53)も1を超えます、ほかに1を超える箇所はありません。
この空白部分に...を表示するようにします。

さて、ここで必要な変数を考えます。

page: 現在表示しているページ(今回の例では50)
lastPage: 最後のページ(今回の例では100)
pageSideLength: 現在表示しているページの前後にいくつ追加するか(今回の例では3)
sideLength: 最初と最後のページ付近にいくつ追加するか(今回の例では2)

ではコードを書きます。


// 塗りつぶす(Fill)クラス。
class FillMap
{
    private array $map = [];

    // $validatorは1からlastPageまでの範囲であるかをチェックするコールバック関数です。
    public function __construct(private $validator)
    {

    }

    public function fill(int $begin, int $end): self
    {
        for($i=$begin; $i<=$end; $i++)
        {
            /** @var callable $val */
            $validate = $this->validator;

            if(!in_array($i, $this->map) && $validate($i))
            {
                $this->map[] = $i;
            }
        }

        return $this;
    }

    public function toArray(): array
    {
        return [...$this->map];
    }

}

// ページネーションの計算するクラス
class PageNavigator
{

    public function __construct(
        private int $page,
        private int $lastPage,
        private int $pageSideLength = 3,
        private int $sideLength = 2
    )
    {

    }

    public function rangeValidate(int $current): bool
    {
        return $current >= 1 && $current <= $this->lastPage;
    }

    public function getPageMap(): array
    {
        // 今回の例では (1, 3) (47, 53) (98, 100) までを塗りつぶすぞ!
        return (new FillMap(fn(int $current) => $this->rangeValidate($current)))
            ->fill(...$this->getFirstRange())
            ->fill(...$this->getPageRange())
            ->fill(...$this->getLastRange())
            ->toArray();
    }

    public function getFirstRange(): array
    {
        return [1, 1 + $this->sideLength];
    }

    public function getPageRange(): array
    {
        return [$this->page - $this->pageSideLength, $this->page + $this->pageSideLength];
    }

    public function getLastRange(): array
    {
        return [$this->lastPage - $this->sideLength, $this->lastPage];
    }

    public function isFirst(int $current): bool { return $current === 1; }
    public function isPage(int $current): bool { return $current === $this->page; }
    public function isLast(int $current): bool { return $current === $this->lastPage; }

    public function isSpaceBefore(array $map, int $current): bool
    {
        // 今回の例では (47 - 3) (98 - 53) の2つの空間で点々追加するぞ!
        return $this->rangeValidate($current) && !in_array($current - 1, $map);
    }

    public function isSpaceAfter(array $map, int $current): bool
    {
        return $this->rangeValidate($current) && !in_array($current + 1, $map);
    }

}

// ------------------ ↑ここまでがライブラリ -------------------

// ------------------ ↓こっから利用する側 -------------------

// 実際に表示してみる関数
function rendererNav(int $page, int $lastPage)
{
    $nav = new PageNavigator($page, $lastPage);
    $map = $nav->getPageMap();

    $links = [];

    foreach($map as $current)
    {

        // 最初、最後よりも現在表示しているページの方が優先度高いです。
        if($nav->isPage($current))
        {
            $links[] = "($current)";
        }
        elseif($nav->isFirst($current))
        {
            $links[] = "[$current]";
        }
        elseif($nav->isLast($current))
        {
            $links[] = "[$current]";
        }
        else
        {

            // 現在のcurrentを表示する前に空白があれば点々を加える
            if($nav->isSpaceBefore($map, $current))
            {
                $links[] = "...";
            }

            $links[] = "$current";
        }
    }

    echo join(' ', $links);
    echo "\n";
}

// 以下テストコード

rendererNav(50, 100);

rendererNav(1, 1);
rendererNav(1, 2);
rendererNav(2, 2);
rendererNav(2, 3);
rendererNav(1, 3);
rendererNav(3, 3);
rendererNav(2, 4);

rendererNav(1, 17);
rendererNav(2, 17);
rendererNav(3, 17);
rendererNav(4, 17);
rendererNav(5, 17);
rendererNav(6, 17);
rendererNav(7, 17);
rendererNav(8, 17);
rendererNav(9, 17);
rendererNav(10, 17);
rendererNav(11, 17);
rendererNav(12, 17);
rendererNav(13, 17);
rendererNav(14, 17);
rendererNav(15, 17);
rendererNav(16, 17);
rendererNav(17, 17);

結果はこうなります。

[1] 2 3 ... 47 48 49 (50) 51 52 53 ... 98 99 [100]
(1)
(1) [2]
[1] (2)
[1] (2) [3]
(1) 2 [3]
[1] 2 (3)
[1] (2) 3 [4]
(1) 2 3 4 ... 15 16 [17]
[1] (2) 3 4 5 ... 15 16 [17]
[1] 2 (3) 4 5 6 ... 15 16 [17]
[1] 2 3 (4) 5 6 7 ... 15 16 [17]
[1] 2 3 4 (5) 6 7 8 ... 15 16 [17]
[1] 2 3 4 5 (6) 7 8 9 ... 15 16 [17]
[1] 2 3 4 5 6 (7) 8 9 10 ... 15 16 [17]
[1] 2 3 ... 5 6 7 (8) 9 10 11 ... 15 16 [17]
[1] 2 3 ... 6 7 8 (9) 10 11 12 ... 15 16 [17]
[1] 2 3 ... 7 8 9 (10) 11 12 13 ... 15 16 [17]
[1] 2 3 ... 8 9 10 (11) 12 13 14 15 16 [17]
[1] 2 3 ... 9 10 11 (12) 13 14 15 16 [17]
[1] 2 3 ... 10 11 12 (13) 14 15 16 [17]
[1] 2 3 ... 11 12 13 (14) 15 16 [17]
[1] 2 3 ... 12 13 14 (15) 16 [17]
[1] 2 3 ... 13 14 15 (16) [17]
[1] 2 3 ... 14 15 16 (17)

renderNavi()は、最初の引数が現在のページ(page)で、二番目の引数が最後のページ(lastPage)です。

rendererNav(50, 100)

今回の例通りの結果になりました。

[1] 2 3 ... 47 48 49 (50) 51 52 53 ... 98 99 [100]

他にバグになりそうな箇所も吟味していきます。
ページ数が少ない場合をざっくり見ていくと、

rendererNav(1, 1) > (1)

1ページしかない場合、現在ページの()が優先されます。最初/最後の括弧ではありません。

rendererNav(1, 2) > (1) [2]

2ページあり、現在1ページめの表示だと、
1は最初のページ[1]でもありますが、現在ページが優先されるので(1)が表示されます。
2が最後のページ[2]

とりあえずPageNavigator(とFillMap)作っておけばある程度自由にデザイン出来るようになります。

BlockEditor certificate css DataGrid Docker Gutenberg Hyper-V iframe MUI openssl PHP React ReduxToolkit REST ubuntu WordPress オレオレ認証局 フレームワーク