├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── Data └── Pagination.hs ├── LICENSE.md ├── README.md ├── cabal.project ├── pagination.cabal └── tests └── Main.hs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | jobs: 11 | ormolu: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: haskell-actions/run-ormolu@v15 16 | build: 17 | runs-on: ubuntu-latest 18 | needs: ormolu 19 | strategy: 20 | matrix: 21 | cabal: ["3.12"] 22 | ghc: ["9.8.4", "9.10.1", "9.12.1"] 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: haskell-actions/setup@v2 26 | id: setup-haskell-cabal 27 | with: 28 | ghc-version: ${{ matrix.ghc }} 29 | cabal-version: ${{ matrix.cabal }} 30 | - run: cabal update 31 | - run: cabal freeze 32 | - uses: actions/cache@v4.0.0 33 | with: 34 | path: | 35 | ${{ steps.setup-haskell-cabal.outputs.cabal-store }} 36 | dist-newstyle 37 | key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} 38 | restore-keys: | 39 | ${{ runner.os }}-${{ matrix.ghc }}- 40 | - run: cabal format && git diff --exit-code --color=always 41 | - run: cabal build 42 | - run: cabal test 43 | - run: cabal haddock 44 | - run: cabal sdist 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *# 2 | *.aux 3 | *.chi 4 | *.chs.h 5 | *.dyn_hi 6 | *.dyn_o 7 | *.eventlog 8 | *.hi 9 | *.hp 10 | *.o 11 | *.prof 12 | *~ 13 | .HTF/ 14 | .cabal-sandbox/ 15 | .ghc.environment.* 16 | .hpc 17 | .hsenv 18 | .stack-work/ 19 | .virtualenv 20 | TAGS 21 | benchmarks.tix 22 | cabal-dev 23 | cabal.config 24 | cabal.project.local 25 | cabal.sandbox.config 26 | dist-*/ 27 | dist/ 28 | hie.yaml 29 | stack.yaml 30 | stack.yaml.lock 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Pagination 0.2.2 2 | 3 | * Works with 9.0.1. Dropped support for GHC 8.6 and older. 4 | 5 | ## Pagination 0.2.1 6 | 7 | * Fix test suite failure with `QuickCheck-2.10`. 8 | 9 | ## Pagination 0.2.0 10 | 11 | * Drop the `Applicative` instance of `Paginated` as it may lead to confusing 12 | results in certain cases. 13 | 14 | * Improved documentation and metadata. 15 | 16 | ## Pagination 0.1.1 17 | 18 | * Relax constraint of `paginate`. We only need `Functor` here, not `Monad`. 19 | 20 | ## Pagination 0.1.0 21 | 22 | * Initial release. 23 | -------------------------------------------------------------------------------- /Data/Pagination.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | {-# LANGUAGE DeriveFunctor #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | 6 | -- | 7 | -- Module : Data.Pagination 8 | -- Copyright : © 2016–present Mark Karpov 9 | -- License : BSD 3 clause 10 | -- 11 | -- Maintainer : Mark Karpov 12 | -- Stability : experimental 13 | -- Portability : portable 14 | -- 15 | -- Framework-agnostic pagination boilerplate. 16 | module Data.Pagination 17 | ( -- * Pagination settings 18 | Pagination, 19 | mkPagination, 20 | pageSize, 21 | pageIndex, 22 | 23 | -- * Paginated data 24 | Paginated, 25 | paginate, 26 | paginatedItems, 27 | paginatedPagination, 28 | paginatedPagesTotal, 29 | paginatedItemsTotal, 30 | hasOtherPages, 31 | pageRange, 32 | hasPrevPage, 33 | hasNextPage, 34 | backwardEllip, 35 | forwardEllip, 36 | 37 | -- * Exceptions 38 | PaginationException (..), 39 | ) 40 | where 41 | 42 | import Control.DeepSeq 43 | import Control.Monad.Catch 44 | import Data.Data (Data) 45 | import Data.List.NonEmpty (NonEmpty (..)) 46 | import Data.List.NonEmpty qualified as NE 47 | import GHC.Generics 48 | import Numeric.Natural 49 | 50 | ---------------------------------------------------------------------------- 51 | -- Pagination settings 52 | 53 | -- | Pagination settings. 54 | data Pagination = Pagination Natural Natural 55 | deriving (Eq, Show, Data, Generic) 56 | 57 | instance NFData Pagination 58 | 59 | -- | Create a 'Pagination' value. May throw 'PaginationException'. 60 | mkPagination :: 61 | (MonadThrow m) => 62 | -- | Page size 63 | Natural -> 64 | -- | Page index 65 | Natural -> 66 | -- | Pagination settings 67 | m Pagination 68 | mkPagination size index 69 | | size == 0 = throwM ZeroPageSize 70 | | index == 0 = throwM ZeroPageIndex 71 | | otherwise = return (Pagination size index) 72 | 73 | -- | Get the page size (the maximum number of items on a page) from a 74 | -- 'Pagination'. 75 | pageSize :: Pagination -> Natural 76 | pageSize (Pagination size _) = size 77 | 78 | -- | Get the page index from a 'Pagination'. 79 | pageIndex :: Pagination -> Natural 80 | pageIndex (Pagination _ index) = index 81 | 82 | ---------------------------------------------------------------------------- 83 | -- Paginated data 84 | 85 | -- | Data in the paginated form. 86 | data Paginated a = Paginated 87 | { pgItems :: [a], 88 | pgPagination :: Pagination, 89 | pgPagesTotal :: Natural, 90 | pgItemsTotal :: Natural 91 | } 92 | deriving (Eq, Show, Data, Generic, Functor) 93 | 94 | instance (NFData a) => NFData (Paginated a) 95 | 96 | instance Foldable Paginated where 97 | foldr f x = foldr f x . pgItems 98 | 99 | instance Traversable Paginated where 100 | traverse f p = 101 | let g p' xs = p' {pgItems = xs} 102 | in g p <$> traverse f (pgItems p) 103 | 104 | -- | Create paginated data. 105 | paginate :: 106 | (Functor m, Integral n) => 107 | -- | Pagination options 108 | Pagination -> 109 | -- | Total number of items 110 | Natural -> 111 | -- | The element producing callback. The function takes arguments: 112 | -- offset and limit. 113 | (n -> n -> m [a]) -> 114 | -- | The paginated data 115 | m (Paginated a) 116 | paginate (Pagination size index') totalItems f = 117 | r <$> f (fromIntegral offset) (fromIntegral size) 118 | where 119 | r xs = 120 | Paginated 121 | { pgItems = xs, 122 | pgPagination = Pagination size index, 123 | pgPagesTotal = totalPages, 124 | pgItemsTotal = totalItems 125 | } 126 | (whole, rems) = totalItems `quotRem` size 127 | totalPages = max 1 (whole + if rems == 0 then 0 else 1) 128 | index = min index' totalPages 129 | offset = (index - 1) * size 130 | 131 | -- | Get the items for current page. 132 | paginatedItems :: Paginated a -> [a] 133 | paginatedItems = pgItems 134 | 135 | -- | Get 'Pagination' parameters that were used to create this paginated 136 | -- result. 137 | paginatedPagination :: Paginated a -> Pagination 138 | paginatedPagination = pgPagination 139 | 140 | -- | Get the total number of pages in this collection. 141 | paginatedPagesTotal :: Paginated a -> Natural 142 | paginatedPagesTotal = pgPagesTotal 143 | 144 | -- | Get the total number of items in this collection. 145 | paginatedItemsTotal :: Paginated a -> Natural 146 | paginatedItemsTotal = pgItemsTotal 147 | 148 | -- | Test whether there are other pages. 149 | hasOtherPages :: Paginated a -> Bool 150 | hasOtherPages Paginated {..} = pgPagesTotal > 1 151 | 152 | -- | Is there previous page? 153 | hasPrevPage :: Paginated a -> Bool 154 | hasPrevPage Paginated {..} = pageIndex pgPagination > 1 155 | 156 | -- | Is there next page? 157 | hasNextPage :: Paginated a -> Bool 158 | hasNextPage Paginated {..} = pageIndex pgPagination < pgPagesTotal 159 | 160 | -- | Get the range of pages to show before and after the current page. This 161 | -- does not necessarily include the first and the last pages (they are 162 | -- supposed to be shown in all cases). Result of the function is always 163 | -- sorted. 164 | pageRange :: 165 | -- | Paginated data 166 | Paginated a -> 167 | -- | Number of pages to show before and after 168 | Natural -> 169 | -- | Page range 170 | NonEmpty Natural 171 | pageRange Paginated {..} 0 = NE.fromList [pageIndex pgPagination] 172 | pageRange Paginated {..} n = 173 | let len = min pgPagesTotal (n * 2 + 1) 174 | index = pageIndex pgPagination 175 | shift 176 | | index <= n = 0 177 | | index >= pgPagesTotal - n = pgPagesTotal - len 178 | | otherwise = index - n - 1 179 | in (+ shift) <$> NE.fromList [1 .. len] 180 | 181 | -- | Backward ellipsis appears when page range (pages around current page to 182 | -- jump to) has gap between its beginning and the first page. 183 | backwardEllip :: 184 | -- | Paginated data 185 | Paginated a -> 186 | -- | Number of pages to show before and after 187 | Natural -> 188 | Bool 189 | backwardEllip p n = NE.head (pageRange p n) > 2 190 | 191 | -- | Forward ellipsis appears when page range (pages around current page to 192 | -- jump to) has gap between its end and the last page. 193 | forwardEllip :: 194 | -- | Paginated data 195 | Paginated a -> 196 | -- | Number of pages to show before and after 197 | Natural -> 198 | -- | Do we have forward ellipsis? 199 | Bool 200 | forwardEllip p@Paginated {..} n = NE.last (pageRange p n) < pred pgPagesTotal 201 | 202 | ---------------------------------------------------------------------------- 203 | -- Exceptions 204 | 205 | -- | Exception indicating various problems when working with paginated data. 206 | data PaginationException 207 | = -- | Page size (number of items per page) was zero 208 | ZeroPageSize 209 | | -- | Page index was zero (they start from one) 210 | ZeroPageIndex 211 | deriving (Eq, Show, Data, Generic) 212 | 213 | instance NFData PaginationException 214 | 215 | instance Exception PaginationException 216 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2016–present Mark Karpov 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | * Neither the name Mark Karpov nor the names of contributors may be used to 16 | endorse or promote products derived from this software without specific 17 | prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY EXPRESS 20 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 21 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 22 | NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 25 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pagination 2 | 3 | [![License BSD3](https://img.shields.io/badge/license-BSD3-brightgreen.svg)](http://opensource.org/licenses/BSD-3-Clause) 4 | [![Hackage](https://img.shields.io/hackage/v/pagination.svg?style=flat)](https://hackage.haskell.org/package/pagination) 5 | [![Stackage Nightly](http://stackage.org/package/pagination/badge/nightly)](http://stackage.org/nightly/package/pagination) 6 | [![Stackage LTS](http://stackage.org/package/pagination/badge/lts)](http://stackage.org/lts/package/pagination) 7 | [![CI](https://github.com/mrkkrp/pagination/actions/workflows/ci.yaml/badge.svg)](https://github.com/mrkkrp/pagination/actions/workflows/ci.yaml) 8 | 9 | The package implements pagination boilerplate in a framework-agnostic way. 10 | 11 | ## Contribution 12 | 13 | Issues, bugs, and questions may be reported in [the GitHub issue tracker for 14 | this project](https://github.com/mrkkrp/pagination/issues). 15 | 16 | Pull requests are also welcome. 17 | 18 | ## License 19 | 20 | Copyright © 2016–present Mark Karpov 21 | 22 | Distributed under BSD 3 clause license. 23 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | packages: . 2 | tests: True 3 | benchmarks: True 4 | constraints: pagination +dev 5 | -------------------------------------------------------------------------------- /pagination.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | name: pagination 3 | version: 0.2.2 4 | license: BSD-3-Clause 5 | license-file: LICENSE.md 6 | maintainer: Mark Karpov 7 | author: Mark Karpov 8 | tested-with: ghc ==9.8.4 ghc ==9.10.1 ghc ==9.12.1 9 | homepage: https://github.com/mrkkrp/pagination 10 | bug-reports: https://github.com/mrkkrp/pagination/issues 11 | synopsis: Framework-agnostic pagination boilerplate 12 | description: Framework-agnostic pagination boilerplate. 13 | category: Data 14 | build-type: Simple 15 | extra-doc-files: 16 | CHANGELOG.md 17 | README.md 18 | 19 | source-repository head 20 | type: git 21 | location: https://github.com/mrkkrp/pagination.git 22 | 23 | flag dev 24 | description: Turn on development settings. 25 | default: False 26 | manual: True 27 | 28 | library 29 | exposed-modules: Data.Pagination 30 | default-language: GHC2021 31 | build-depends: 32 | base >=4.15 && <5, 33 | deepseq >=1.3 && <1.6, 34 | exceptions >=0.6 && <0.11 35 | 36 | if flag(dev) 37 | ghc-options: 38 | -Wall -Werror -Wredundant-constraints -Wpartial-fields 39 | -Wunused-packages 40 | 41 | else 42 | ghc-options: -O2 -Wall 43 | 44 | test-suite tests 45 | type: exitcode-stdio-1.0 46 | main-is: Main.hs 47 | hs-source-dirs: tests 48 | default-language: GHC2021 49 | build-depends: 50 | base >=4.15 && <5, 51 | QuickCheck >=2.10 && <3, 52 | exceptions >=0.6 && <0.11, 53 | hspec >=2 && <3, 54 | pagination 55 | 56 | if flag(dev) 57 | ghc-options: 58 | -Wall -Werror -Wredundant-constraints -Wpartial-fields 59 | -Wunused-packages 60 | 61 | else 62 | ghc-options: -O2 -Wall 63 | -------------------------------------------------------------------------------- /tests/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE RankNTypes #-} 2 | {-# OPTIONS_GHC -fno-warn-orphans #-} 3 | 4 | module Main (main) where 5 | 6 | import Control.Monad 7 | import Control.Monad.Catch (SomeException, fromException) 8 | import Data.List.NonEmpty (NonEmpty (..)) 9 | import Data.List.NonEmpty qualified as NE 10 | import Data.Maybe (fromJust) 11 | import Data.Pagination 12 | import Numeric.Natural 13 | import Test.Hspec 14 | import Test.QuickCheck hiding (total) 15 | 16 | main :: IO () 17 | main = hspec spec 18 | 19 | spec :: Spec 20 | spec = do 21 | describe "mkPagination" $ do 22 | context "when page size is zero" $ 23 | it "throws ZeroPageSize exception" $ 24 | property $ \index -> 25 | asEither (mkPagination 0 index) === Left ZeroPageSize 26 | context "when page index in zero" $ 27 | it "throws ZeroPageIndex exception" $ 28 | property $ \size -> 29 | size > 0 ==> asEither (mkPagination size 0) === Left ZeroPageIndex 30 | context "when page size and page index are positive" $ 31 | it "we get the Pagination value" $ 32 | property $ \size index -> 33 | (size > 0 && index > 0) ==> do 34 | p <- mkPagination size index 35 | pageSize p `shouldBe` size 36 | pageIndex p `shouldBe` index 37 | describe "Functor instance of Paginated" $ 38 | it "works" $ 39 | property $ \r -> 40 | let f :: Int -> Int 41 | f = (+ 1) 42 | in paginatedItems (f <$> r) === (f <$> paginatedItems r) 43 | describe "Foldable instance of Paginated" $ 44 | it "foldr works like with lists" $ 45 | property $ \p n -> 46 | let f :: (Foldable f) => f Int -> Int 47 | f = foldr (+) n 48 | in f p === f (paginatedItems p) 49 | describe "Traversable instance of Paginated" $ 50 | it "traverse works like with lists" $ 51 | property $ \p -> 52 | (paginatedItems <$> traverse Just (p :: Paginated Int)) 53 | === Just (paginatedItems p) 54 | describe "paginate" $ 55 | context "when total number of items is zero" $ 56 | it "produces an empty pagination" $ 57 | property $ \p n -> do 58 | r <- paginate p 0 $ \offset limit -> do 59 | offset `shouldBe` 0 60 | limit `shouldBe` pageSize p 61 | return [] 62 | paginatedItems r `shouldBe` ([] :: [Int]) 63 | (pageSize . paginatedPagination) r `shouldBe` pageSize p 64 | (pageIndex . paginatedPagination) r `shouldBe` 1 65 | paginatedPagesTotal r `shouldBe` 1 66 | paginatedItemsTotal r `shouldBe` 0 67 | pageRange r n `shouldBe` 1 :| [] 68 | hasOtherPages r `shouldBe` False 69 | hasPrevPage r `shouldBe` False 70 | hasNextPage r `shouldBe` False 71 | backwardEllip r n `shouldBe` False 72 | forwardEllip r n `shouldBe` False 73 | describe "paginatedItems" $ 74 | it "number of actual items is less than or equal to page size" $ 75 | property $ \r -> 76 | let size = pageSize (paginatedPagination (r :: Paginated Int)) 77 | in (fromIntegral . length . paginatedItems) r `shouldSatisfy` (<= size) 78 | describe "paginatedPagination" $ 79 | it "returns original pagination correcting index if necessary" $ 80 | property $ \p n -> do 81 | r <- paginate p n $ \offset limit -> do 82 | let totalPages = ptotal n (pageSize p) 83 | offset 84 | `shouldBe` min 85 | ((pageIndex p - 1) * pageSize p) 86 | ((totalPages - 1) * pageSize p) 87 | limit `shouldBe` pageSize p 88 | return (replicate (plen n offset limit) (0 :: Int)) 89 | pageSize (paginatedPagination r) `shouldBe` pageSize p 90 | pageIndex (paginatedPagination r) `shouldSatisfy` (<= pageIndex p) 91 | describe "paginatedPagesTotal" $ 92 | it "returns correct number of total pages" $ 93 | property $ \r -> 94 | let itemsTotal = paginatedItemsTotal (r :: Paginated Int) 95 | psize = pageSize (paginatedPagination r) 96 | in paginatedPagesTotal r `shouldBe` ptotal itemsTotal psize 97 | describe "paginatedItemsTotal" $ 98 | it "returns the same number of items as it was specified for paginate" $ 99 | property $ \p n -> do 100 | r <- paginate p n ((\_ _ -> return []) :: Int -> Int -> IO [Int]) 101 | paginatedItemsTotal r `shouldBe` n 102 | describe "hasOtherPages" $ 103 | it "correctly detects whether we the collection has other pages" $ 104 | property $ \r -> 105 | hasOtherPages (r :: Paginated Int) `shouldBe` paginatedPagesTotal r > 1 106 | describe "hasPrevPage" $ 107 | it "correctly detect whether paginated data has previous page" $ 108 | property $ \r -> 109 | hasPrevPage (r :: Paginated Int) 110 | === (pageIndex (paginatedPagination r) /= 1) 111 | describe "hasNextPage" $ 112 | it "correctly detect whether paginated data has next page" $ 113 | property $ \r -> 114 | hasNextPage (r :: Paginated Int) 115 | === (pageIndex (paginatedPagination r) /= paginatedPagesTotal r) 116 | describe "pageRange" $ 117 | it "correctly performs generation of page ranges" $ 118 | forM_ [1 .. 10] $ 119 | \n -> do 120 | p <- mkPagination 10 n 121 | r <- paginate p 95 (\_ limit -> return [1 .. limit]) 122 | let x = NE.toList (pageRange (r :: Paginated Int) 2) 123 | x `shouldBe` case n of 124 | 1 -> [1 .. 5] 125 | 2 -> [1 .. 5] 126 | 3 -> [1 .. 5] 127 | 4 -> [2 .. 6] 128 | 5 -> [3 .. 7] 129 | 6 -> [4 .. 8] 130 | 7 -> [5 .. 9] 131 | 8 -> [6 .. 10] 132 | 9 -> [6 .. 10] 133 | _ -> [6 .. 10] 134 | describe "backwardEllip" $ 135 | it "correctly detects when there is a backward ellipsis" $ 136 | property $ \r n -> 137 | backwardEllip (r :: Paginated Int) n 138 | === (NE.head (pageRange r n) > 2) 139 | describe "forwardEllip" $ 140 | it "correctly detects when there is a forward ellipsis" $ 141 | property $ \r n -> 142 | forwardEllip (r :: Paginated Int) n 143 | === (NE.last (pageRange r n) < paginatedPagesTotal r - 1) 144 | 145 | ---------------------------------------------------------------------------- 146 | -- Arbitrary instances 147 | 148 | instance Arbitrary Natural where 149 | arbitrary = fromInteger . getNonNegative <$> arbitrary 150 | 151 | instance Arbitrary Pagination where 152 | arbitrary = do 153 | size <- p 154 | index <- p 155 | (return . fromJust) (mkPagination size index) 156 | where 157 | p = arbitrary `suchThat` (> 0) 158 | 159 | instance (Arbitrary a) => Arbitrary (Paginated a) where 160 | arbitrary = do 161 | pagination <- arbitrary 162 | total <- arbitrary 163 | let f offset limit = vector (plen total offset limit) 164 | paginate pagination total f 165 | 166 | ---------------------------------------------------------------------------- 167 | -- Helpers 168 | 169 | -- | Run a computation inside 'MonadThrow' and return its result as an 170 | -- 'Either'. 171 | asEither :: Either SomeException a -> Either PaginationException a 172 | asEither = either (Left . fromJust . fromException) Right 173 | 174 | -- | Calculate number of items in paginated selection given total number of 175 | -- items, offset, and limit. 176 | plen :: 177 | (Integral n) => 178 | -- | Total items 179 | Natural -> 180 | -- | Offset 181 | Natural -> 182 | -- | Limit 183 | Natural -> 184 | n 185 | plen total offset limit = fromIntegral (min (total - offset) limit) 186 | 187 | -- | Calculate the total number of pages given the total number of items, 188 | -- and the page size. 189 | ptotal :: 190 | (Integral n) => 191 | -- | Total items 192 | Natural -> 193 | -- | Page size 194 | Natural -> 195 | n 196 | ptotal total size = 197 | fromIntegral $ 198 | let (whole, rems) = total `quotRem` size 199 | in max 1 (whole + if rems == 0 then 0 else 1) 200 | --------------------------------------------------------------------------------