Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add p/avl/pager #2584

Merged
merged 28 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/gno.land/p/demo/avl/pager/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module gno.land/p/demo/avl/pager

require (
gno.land/p/demo/avl v0.0.0-latest
gno.land/p/demo/uassert v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/demo/urequire v0.0.0-latest
)
220 changes: 220 additions & 0 deletions examples/gno.land/p/demo/avl/pager/pager.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package pager

import (
"math"
"net/url"
"strconv"

"gno.land/p/demo/avl"
"gno.land/p/demo/ufmt"
)

// Pager is a struct that holds the AVL tree and pagination parameters.
type Pager struct {
Tree *avl.Tree
PageQueryParam string
SizeQueryParam string
DefaultPageSize int
}

// Page represents a single page of results.
type Page struct {
Items []Item
PageNumber int
PageSize int
TotalItems int
TotalPages int
HasPrev bool
HasNext bool
Pager *Pager // Reference to the parent Pager
}

// Item represents a key-value pair in the AVL tree.
type Item struct {
Key string
Value interface{}
}

// NewPager creates a new Pager with default values.
func NewPager(tree *avl.Tree, defaultPageSize int) *Pager {
return &Pager{
Tree: tree,
PageQueryParam: "page",
SizeQueryParam: "size",
DefaultPageSize: defaultPageSize,
}
}

// GetPage retrieves a page of results from the AVL tree.
func (p *Pager) GetPage(pageNumber int) *Page {
return p.GetPageWithSize(pageNumber, p.DefaultPageSize)
}

func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page {
totalItems := p.Tree.Size()
totalPages := int(math.Ceil(float64(totalItems) / float64(pageSize)))

page := &Page{
TotalItems: totalItems,
TotalPages: totalPages,
PageSize: pageSize,
Pager: p,
}

// pages without content
if pageSize < 1 {
return page
}

// page number provided is not available
if pageNumber < 1 {
page.HasNext = totalPages > 0
return page
}

// page number provided is outside the range of total pages
if pageNumber > totalPages {
page.PageNumber = pageNumber
page.HasPrev = pageNumber > 0
Copy link
Contributor

@leohhhn leohhhn Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
page.HasPrev = pageNumber > 0
page.HasPrev = true

pageNumber > 0 will always be true in this if, because totalPages cannot be less than 0

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, but actually i'm considering if in this case it should maybe be totalPages > 0.

return page
}

startIndex := (pageNumber - 1) * pageSize
endIndex := startIndex + pageSize
if endIndex > totalItems {
endIndex = totalItems
}

items := []Item{}
p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool {
items = append(items, Item{Key: key, Value: value})
return false
Copy link
Contributor

@ajnavarro ajnavarro Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

offtopic: a bit misleading. Normally, if you want to continue, you return true, but it is what it is. Example from standard library: https://pkg.go.dev/sync#Map.Range

})

page.Items = items
page.PageNumber = pageNumber
page.HasPrev = pageNumber > 1
page.HasNext = pageNumber < totalPages
return page
}

func (p *Pager) MustGetPageByPath(rawURL string) *Page {
page, err := p.GetPageByPath(rawURL)
if err != nil {
panic("invalid path")
}
return page
}

// GetPageByPath retrieves a page of results based on the query parameters in the URL path.
func (p *Pager) GetPageByPath(rawURL string) (*Page, error) {
pageNumber, pageSize, err := p.ParseQuery(rawURL)
if err != nil {
return nil, err
}
return p.GetPageWithSize(pageNumber, pageSize), nil
}

// UI generates the Markdown UI for the page selector.
func (p *Page) Selector() string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (p *Page) Selector() string {
func (p *Page) Picker() string {

I think this is a more intuitive name

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, Picker sounds better. @alexiscolin, do you have an opinion on the name of this part, "1 2 3 4 5"?

pageNumber := p.PageNumber
pageNumber = max(pageNumber, 1)

if p.TotalPages <= 1 {
return ""
}

md := ""

if p.HasPrev {
// Always show the first page link
md += ufmt.Sprintf("[%d](?%s=%d) | ", 1, p.Pager.PageQueryParam, 1)

// Before
if p.PageNumber > 4 {
md += "… | "
}

if p.PageNumber > 3 {
md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber-2, p.Pager.PageQueryParam, p.PageNumber-2)
}

if p.PageNumber > 2 {
md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber-1, p.Pager.PageQueryParam, p.PageNumber-1)
}
}

if p.PageNumber > 0 && p.PageNumber <= p.TotalPages {
// Current page
md += ufmt.Sprintf("**%d**", p.PageNumber)
} else {
md += ufmt.Sprintf("_%d_", p.PageNumber)
}

if p.HasNext {
md += " | "

if p.PageNumber < p.TotalPages-1 {
md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber+1, p.Pager.PageQueryParam, p.PageNumber+1)
}

if p.PageNumber < p.TotalPages-2 {
md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber+2, p.Pager.PageQueryParam, p.PageNumber+2)
}

if p.PageNumber < p.TotalPages-3 {
md += "… | "
}

// Always show the last page link
md += ufmt.Sprintf("[%d](?%s=%d)", p.TotalPages, p.Pager.PageQueryParam, p.TotalPages)
}

return md
}

// ParseQuery parses the URL to extract the page number and page size.
func (p *Pager) ParseQuery(rawURL string) (int, int, error) {
u, err := url.Parse(rawURL)
if err != nil {
return 1, p.DefaultPageSize, err
}

query := u.Query()
pageNumber := 1
pageSize := p.DefaultPageSize

if p.PageQueryParam != "" {
Copy link
Contributor

@ltzmaxwell ltzmaxwell Oct 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: given the current situation, this parameter will not be empty, so the check can be omitted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be made empty. The question is whether we let the if condition so that the subsequent line doesn't produce something strange, or if we switch to a panic because it's nonsensical to use a pager without a way to change the query string.

if pageStr := query.Get(p.PageQueryParam); pageStr != "" {
pageNumber, err = strconv.Atoi(pageStr)
if err != nil || pageNumber < 1 {
pageNumber = 1
}
}
}

if p.SizeQueryParam != "" {
if sizeStr := query.Get(p.SizeQueryParam); sizeStr != "" {
pageSize, err = strconv.Atoi(sizeStr)
if err != nil || pageSize < 1 {
pageSize = p.DefaultPageSize
}
}
}

return pageNumber, pageSize, nil
}

func min(a, b int) int {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems not used in any place

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a < b {
return a
}
return b
}

func max(a, b int) int {
if a > b {
return a
}
return b
}
Loading
Loading