majidarif
majidarif

Reputation: 20025

Procedurally generating pagination

I'm trying to create pagination with Go but I'm a bit confused. It's my first time to creating pagination as I used to use laravel's helper class when I was still using PHP.

I tried doing something like:

var totalPages = int(math.Ceil(float64(totalRecords) / float64(recordsPerPage)))

for i := 0; i < totalPages; i++ {
    pages[i] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, i+1, limit, i+1)
}

And that shows all the pages, I want to create something that would look like:

< 1 2 ... 20 24 25 26 27 ... 200 201 >

25 being current page and 201 being the last page.

I also experimented with something like the following but was quirky on some cases like if the page is close to the start or the end:

// pages[0] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, 1, limit, 1)
// pages[1] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, 2, limit, 2)
// pages[2] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, 3, limit, 3)
// pages[3] = `<li><a class="more">&hellip;</a></li>`
// pages[4] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, page, limit, page)
// pages[5] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, page+1, limit, page+1)
// pages[6] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, page+2, limit, page+2)
// pages[7] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, page+3, limit, page+3)
// pages[8] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, page+4, limit, page+4)
// pages[9] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, page+5, limit, page+5)
// pages[10] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, page+6, limit, page+6)
// pages[11] = `<li><a class="more">&hellip;</a></li>`
// pages[12] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, totalPages-1, limit, totalPages-1)
// pages[13] = fmt.Sprintf(`<li><a href="?page=%d&limit=%d">%d</a></li>`, totalPages, limit, totalPages)

So the question is, how do I achieve this? Is there a library? What is the correct logic?

Upvotes: 2

Views: 141

Answers (2)

icza
icza

Reputation: 417797

Here's a short implementation, which returns the page numbers you need to render:

func pages(cur, max, around int) (r []int) {
    for i := cur - around; i <= cur+around; i++ {
        if i >= 1 && i <= max {
            r = append(r, i)
        }
    }
    for i := 1; i <= around; i++ {
        if i < cur-around {
            r = append(r, i)
        }
        if max+1-i > cur+around {
            r = append(r, max+1-i)
        }
    }
    sort.Ints(r)
    return
}

You need to pass the current page (cur), the max page number (max), and how many neighbors (around) you want to list around current and at the ends of the list

If 2 page numbers next to each other have difference > 1, you also need to render ... between them.

Testing it:

fmt.Println(pages(1, 1, 2))
fmt.Println(pages(1, 2, 2))
fmt.Println(pages(1, 3, 2))
fmt.Println(pages(1, 4, 2))
fmt.Println(pages(1, 5, 2))
fmt.Println(pages(1, 9, 3))
fmt.Println(pages(25, 201, 2))

ps := pages(25, 201, 3)
for i, page := range ps {
    if i > 0 && ps[i-1]+1 < page {
        fmt.Print("... ")
    }
    fmt.Print(page, " ")
}

Output (try it on the Go Playground):

[1]
[1 2]
[1 2 3]
[1 2 3 4]
[1 2 3 4 5]
[1 2 3 4 7 8 9]
[1 2 23 24 25 26 27 200 201]
1 2 3 ... 22 23 24 25 26 27 28 ... 199 200 201 

Eliminating sort.Ints()

The purpose of sort.Ints() is to return page numbers in increasing order. It is needed because the 2nd loop adds numbers out of order.

If we can change it so that the 2nd loop keeps the order, sorting won't be needed anymore.

The 2nd loop is responsible to add page numbers from the ends of the list (beginning and end). Appending end is fine (just have to go upward), and we'll add the beginning to another slice, to which the rest will be appended.

Here it is:

func pages(cur, max, around int) (r []int) {
    for i := cur - around; i <= cur+around; i++ {
        if i >= 1 && i <= max {
            r = append(r, i)
        }
    }
    r2 := make([]int, 0, len(r)+4)
    for i := 1; i <= around; i++ {
        if i < cur-around {
            r2 = append(r2, i)
        }
        if max-around+i > cur+around {
            r = append(r, max-around+i)
        }
    }
    return append(r2, r...)
}

Testing and output is the same. Try this variant on the Go Playground.

Also specifying number of pages at the ends

If you want different number of pages at the ends, you can add 1 additional parameter and use it with 0 complexity added:

func pages(cur, max, around, edge int) (r []int) {
    for i := cur - around; i <= cur+around; i++ {
        if i >= 1 && i <= max {
            r = append(r, i)
        }
    }
    r2 := make([]int, 0, len(r)+2*edge)
    for i := 1; i <= edge; i++ {
        if i < cur-around {
            r2 = append(r2, i)
        }
        if max-around+i > cur+around {
            r = append(r, max-around+i)
        }
    }
    return append(r2, r...)
}

Try this variant on the Go Playground.

Upvotes: 2

programaths
programaths

Reputation: 891

Break down your problem and you will get it.

What you want is to compute the neighbors.

package main

import (
    "fmt"
)

func main() {
    pages(8, 2, 13)
}

func pages(n int, around int, count int) {
    first := n - around
    if first < 1 {
        first = 1
    }
    last := n + around
    if last > count {
        last = count
    }

    if first > 1 {
        for i := 1; i <= 2 && i < first; i++ {
            fmt.Println(i)
        }
        if 3 < first {
            fmt.Println("...")
        }
    }

    for i := first; i <= last; i++ {
        fmt.Println(i)
    }

    if last < count {
        if last <= count-3 {
            fmt.Println("...")
        }
        end := count - 1
        if end <= last {
            end = last + 1
        }
        for i := end; i <= count; i++ {
            fmt.Println(i)
        }

    }
}

Improvement : Make number of "prefix" and "suffix" pages variable ;-)

See: https://play.golang.org/p/wOOO9GmpNV

Added a shortened version :

package main

import (
    "fmt"
)

func main() {
    pages(10, 3, 20)
}

func pages(n int, around int, count int) {
    var i int
    for i = 1; i <= 2 && i<=count; i++ {
        fmt.Println(i)
    }

    if i < n-around {
        fmt.Println("...")
        i = n - around
    }

    for ; i <= n+around && i<=count; i++ {
        fmt.Println(i)
    }

    if i < count-1 {
        fmt.Println("...")
        i = count - 1
    }

    for ; i <= count; i++ {
        fmt.Println(i)
    }
}

We can easily wrap things around by providing a callback. Note that channels are very slow.

package main

import (
    "fmt"
)

func main() {
    pages(10, 3, 20, func(i int) {
        if i < 0 {
            fmt.Println("...")
            return
        }
        fmt.Println(i)
    })
}

func pages(n int, around int, count int, render func(int)) {
    var i int
    for i = 1; i <= 2 && i <= count; i++ {
        render(i)
    }

    if i < n-around {
        render(-1)
        i = n - around
    }

    for ; i <= n+around && i <= count; i++ {
        render(i)
    }

    if i < count-1 {
        render(-1)
        i = count - 1
    }

    for ; i <= count; i++ {
        render(i)
    }
}

Last version (unless bugs) which includes everything :

package main

import (
    "fmt"
)

func main() {
    pages(10, 3, 21,4,4, func(i int) {
        if i < 0 {
            fmt.Println("...")
            return
        }
        fmt.Println(i)
    })
}

func pages(n int, around int, count int,start int, end int, render func(int)) {
    var i int
    for i = 1; i <= start && i <= count; i++ {
        render(i)
    }

    if i < n-around {
        render(-1)
        i = n - around
    }

    for ; i <= n+around && i <= count; i++ {
        render(i)
    }

    if i < count-end+1 {
        render(-1)
        i = count - end+1
    }

    for ; i <= count; i++ {
        render(i)
    }
}

See: https://play.golang.org/p/KfTORNuHY_

How good is this ?

  • Take no more memory than required for parameters and current page.
  • Each check is done once.
  • It follows what a human would do if asked to write down pagination. (natural logic)
  • Caller side only has to focus on rendering of current page or separator.
  • Fast ! The bottleneck is the callback.

Note, if an array is needed, one can do it through a closure. Same with channels.

Upvotes: 2

Related Questions