Pagination in Clojure

Published 2015-07-26 on Farid Zakaria's Blog

Luminus - Pagination

I've recently been working on a fun side project using the Luminus web framework as my first foray into Clojure (which I'm absolutely falling in love with)

One thing however I find missing from the documentation and in general online is an idiomatic way to paginate in Clojure. I'm sure there is some sexy pagination strategy that uses lazy-seqs, macros, protocols and records however I was not able to come up with anything (myself or via google).

I'm dumping my small helper functions that I ended up writing in hopes that perhaps someone finds use for it:

(ns sample.paginate)

(defn parse-number
  "Reads a number from a string. Returns nil if not a number."
  [s]
  (println (type s))
  (cond
   (isa? (type s) java.lang.Number) s
   (isa? (type s) java.lang.String)
     (if (re-find #"^-?\d+\.?\d*$" s)
       (read-string s))
   :else (parse-number (str s))))

(defn map-kv [m f]
  "Aply a map to a key value map"
  (reduce-kv #(assoc %1 %2 (f %3)) {} m))

(def default-page 1)

(def default-size 20)

(def min-size 1)

(def min-page 1)

(def page-key :page)

(def size-key :size)

(def next-page-key :next-page)

(def prev-page-key :prev-page)

(def offset-key :offset)

(def default-paginate-params
  {page-key default-page size-key default-size})

(defn extract [request]
  "Given a request extracts the page and size from the request object.
   If none is found it returns sensible defaults. It makes sure the returned
   paginate values are integers"
  (let [params (:params request)
        params (merge default-paginate-params params)
        paginate (select-keys params [page-key size-key])]
    (map-kv paginate parse-number)))

(defn current-page [request]
  (let [paginate-params (extract request)]
    (max (page-key paginate-params) min-page)))

(defn next-page [request]
  (let [page (current-page request)]
    (inc page)))

(defn prev-page [request]
  (let [page (current-page request)]
    (max min-page (dec page))))

(defn size [request]
  (let [paginate-params (extract request)]
    (max (size-key paginate-params) min-size)))

(defn offset [request]
  "Determines the offset for the page. The offset is calculated based on
   (page - 1) * size"
  (* (dec (current-page request)) (size request)))
        
       
(defn create [request]
  "Creates a paginate map which contains additional keys such as next-page, prev-page
   ontop of the page and size keys"
  (let [page (current-page request)
        size (size request)
        offset (offset request)
        next-page (next-page request)
        prev-page (prev-page request)]
    {page-key page size-key size next-page-key next-page prev-page-key prev-page offset-key offset}))

Ultimately one would use the create function to include in their context/response a structured Pagination map.

If you have anything better please share!