steid

@jamesgill /

Rust5.0 KBCode·Blame·History·Raw
1//! The arithmetic every paged list shares.
2//!
3//! **Offset paging, one page size per list, 1-based page numbers** — see
4//! [ui.md](../../plans/ui.md#paging) for why, including why the git-backed lists page
5//! by `--skip` rather than by a keyset cursor.
6//!
7//! It lives here rather than in `web` because paging is part of what a use case
8//! answers: a page number arrives from a URL, but "there is no page 9" is a fact about
9//! the list, and a second transport asking the same use case must get the same answer.
10//!
11//! A page number out of range is **clamped, never refused**. A deep link goes stale for
12//! ordinary reasons — a branch is rewritten, issues are closed, a hook's deliveries age
13//! out — and the honest answer to `?page=400` on a four-page list is the last page a
14//! reader can actually see, not a 500.
15
16/// A page of a list, and enough about the whole to render a control.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Paged<T> {
19 pub items: Vec<T>,
20 /// How long the whole list is, so the control can say "of 7" and the sentence "of
21 /// 312".
22 pub total: usize,
23 /// The 1-based page `items` came from, **after clamping** — what the page renders as
24 /// current, so a clamped request marks the page it actually shows.
25 pub page: usize,
26 /// How many pages the whole list makes. Never fewer than one.
27 pub pages: usize,
28 /// The page size this was cut at, carried so a caller need not remember which
29 /// constant a given list uses.
30 pub size: usize,
31}
32
33impl<T> Paged<T> {
34 /// Cuts a page out of a list already held in memory.
35 ///
36 /// The right shape for the lists whose whole contents arrive in one go anyway — the
37 /// branches and tags of one `for-each-ref`, a profile's repositories after the
38 /// visibility filter — where a `limit`/`offset` on the port would buy nothing and
39 /// cost a second query for the count.
40 pub fn slice(items: Vec<T>, page: usize, size: usize) -> Self {
41 let total = items.len();
42 let pages = page_count(total, size);
43 let page = clamp(page, pages);
44
45 let items = items
46 .into_iter()
47 .skip(offset(page, size))
48 .take(size)
49 .collect();
50
51 Self {
52 items,
53 total,
54 page,
55 pages,
56 size,
57 }
58 }
59
60 /// A page assembled by whatever counted and queried it — a port with `limit` and
61 /// `offset`, or git's `--skip`.
62 pub fn of(items: Vec<T>, total: usize, page: usize, size: usize) -> Self {
63 let pages = page_count(total, size);
64
65 Self {
66 items,
67 total,
68 page: clamp(page, pages),
69 pages,
70 size,
71 }
72 }
73
74 /// Where this page starts in the whole list, in items.
75 pub fn offset(&self) -> usize {
76 offset(self.page, self.size)
77 }
78
79 /// Whether there is more than one page — the only condition under which a list says
80 /// anything about paging at all.
81 pub fn is_paged(&self) -> bool {
82 self.pages > 1
83 }
84}
85
86/// How many pages `total` items make at `size` each.
87///
88/// Never fewer than one: an empty list is page 1 of 1, which is exactly the case that
89/// renders no control.
90pub fn page_count(total: usize, size: usize) -> usize {
91 total.div_ceil(size.max(1)).max(1)
92}
93
94/// A requested page number, brought inside `1..=pages`.
95pub fn clamp(page: usize, pages: usize) -> usize {
96 page.clamp(1, pages.max(1))
97}
98
99/// Where a 1-based page starts, in items.
100pub fn offset(page: usize, size: usize) -> usize {
101 page.saturating_sub(1) * size
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn an_empty_list_is_one_page() {
110 assert_eq!(page_count(0, 50), 1);
111 assert_eq!(page_count(50, 50), 1);
112 assert_eq!(page_count(51, 50), 2);
113 assert_eq!(page_count(100, 50), 2);
114 }
115
116 #[test]
117 fn an_offset_is_the_pages_before_it() {
118 assert_eq!(offset(1, 50), 0);
119 assert_eq!(offset(3, 50), 100);
120 assert_eq!(offset(0, 50), 0);
121 }
122
123 #[test]
124 fn the_second_page_starts_where_the_first_ended() {
125 let items: Vec<usize> = (0..7).collect();
126
127 let first = Paged::slice(items.clone(), 1, 3);
128 let second = Paged::slice(items, 2, 3);
129
130 assert_eq!(first.items, [0, 1, 2]);
131 assert_eq!(second.items, [3, 4, 5]);
132 assert_eq!(second.offset(), 3);
133 assert_eq!(second.pages, 3);
134 assert_eq!(second.total, 7);
135 }
136
137 #[test]
138 fn a_page_past_the_end_clamps_to_the_last_one() {
139 let paged = Paged::slice((0..7).collect::<Vec<_>>(), 400, 3);
140
141 assert_eq!(paged.page, 3);
142 assert_eq!(paged.items, [6]);
143 }
144
145 #[test]
146 fn a_page_below_the_first_clamps_up() {
147 let paged = Paged::slice((0..7).collect::<Vec<_>>(), 0, 3);
148
149 assert_eq!(paged.page, 1);
150 assert_eq!(paged.items, [0, 1, 2]);
151 }
152
153 #[test]
154 fn a_list_that_fits_is_not_paged() {
155 let paged = Paged::slice((0..3).collect::<Vec<_>>(), 1, 50);
156
157 assert!(!paged.is_paged());
158 assert_eq!(paged.pages, 1);
159 }
160}