Extreme branchless: Pagination seven
Continuing my branchless journey.
This time: Pagination seven kata.
I'm not really a huge fan of this kata as, to me, it mostly consist in distinguishing multiple strategies.
Let's have a minimalist start:
spec =
describe "Pagination seven" $ do
forM_ [
(2, 5, "1 (2) 3 4 5")
] $ \(p, t, expected) ->
it ("Page " <> show p <> " of " <> show t) $
paginate p t `shouldBe` expected
paginate currentPage total = "1 (2) 3 4 5"
Let's force a real implementation with another tests:
-- (6, 7, "1 2 3 4 5 (6) 7")
paginate currentPage total = unwords $ tail $ zipWith3 (\f x y -> f x y) selector simpleNumbers checkedNumbers
where simpleNumbers = show <$> [0 .. natInt total]
checkedNumbers = map (\n -> "(" <> n <> ")") simpleNumbers
selector = runNatural currentPage (const :) (flip const : repeat const)
It works as follows:
simpleNumbers
is a list of simple number (e.g.1 2 3 4 5 6 7
)checkedNumbers
is a list of selected numbers (e.g.(1) (2) (3) (4) (5) (6) (7)
)selector
is a list of functions taking it's first (e.g.const
) or the second (e.g.flip const
)
I zip the three lists, and here we are!
In order to go further, and add ellipses, we have to distinguish the cases depending on the total number of pages.
We can do that by splitting the solution space in a list:
-- (5, 9, "1 … 4 (5) 6 … 9")
paginate currentPage total = at strategies total currentPage
where strategies =
error "no pagination on 0" : map (smallPaginate . fromInteger) [1 .. 7] <> map (bigPaginate . fromInteger) [8 ..]
smallPaginate total currentPage = unwords $ tail $ zipWith3 (\f x y -> f x y) selector simpleNumbers checkedNumbers
where simpleNumbers = show <$> [0 .. natInt total]
checkedNumbers = map (\n -> "(" <> n <> ")") simpleNumbers
selector = runNatural currentPage (const :) (flip const : repeat const)
bigPaginate total currentPage = "1 … 4 (5) 6 … 9"
Let's add another test to force a real implementation:
-- (42, 100, "1 … 41 (42) 43 … 100")
paginate currentPage total = at strategies total currentPage
where strategies =
error "no pagination on 0" : map (smallPaginate . fromInteger) [1 .. 7] <> map (bigPaginate . fromInteger) [8 ..]
bigPaginate total currentPage = "1 … " <> at centers currentPage <> " … " <> show total
where centers = map (\n -> show n <> " (" <> show (n + 1) <> ") " <> show (n + 2)) [-1 :: Int .. ]
Then, when have to handle when the current page is close to the start, again, we can split the solution space with a list, this time indexed on the current page:
-- (4, 9, "1 2 3 (4) 5 … 9")
paginate currentPage total = at strategies total currentPage
where strategies =
error "no pagination on 0" : map (smallPaginate . fromInteger) [1 .. 7] <> map (bigPaginate . fromInteger) [8 ..]
bigPaginate total currentPage = at strategies currentPage
where strategies =
error "no pagination on 0"
: map (\n -> smallPaginate 5 (fromInteger n) <> " … " <> show total) [1 .. 4] <> repeat onCenter
onCenter = "1 … " <> at centers currentPage <> " … " <> show total
centers = map (\n -> show n <> " (" <> show (n + 1) <> ") " <> show (n + 2)) [-1 :: Int .. ]
Lastly, we have to handle when current is close to the end.
To simplify the implementation, we'll generalize and reuse smallPaginate
:
-- (6, 9, "1 … 5 (6) 7 8 9")
paginate currentPage total = at strategies total currentPage
where strategies =
error "no pagination on 0"
: map (smallPaginate . fromInteger) [1 .. 7]
<> map (bigPaginate . fromInteger) [8 ..]
smallPaginate = smallPaginateFrom 0
smallPaginateFrom from total currentPage =
unwords $ runNatural from tail $ tail $ zipWith3 (\f x y -> f x y) selector simpleNumbers checkedNumbers
where simpleNumbers = show <$> [0 .. natInt total]
checkedNumbers = map (\n -> "(" <> n <> ")") simpleNumbers
selector = runNatural currentPage (const :) (flip const : repeat const)
bigPaginate total currentPage = at strategies currentPage
where strategies =
error "no pagination on 0"
: map (\n -> smallPaginate 5 (fromInteger n) <> " … " <> show total) [1 .. 4]
<> replicate (natInt $ total - 8) onCenter
<> map (\n -> "1 … " <> smallPaginateFrom (total - 5) total (total - 4 + fromInteger n)) [1 .. 4]
onCenter = "1 … " <> at centers currentPage <> " … " <> show total
centers = map (\n -> show n <> " (" <> show (n + 1) <> ") " <> show (n + 2)) [-1 :: Int .. ]
Here we are!