pdfrum_common/page_index.rs
1//! A page's position in the document, as a type rather than a bare integer.
2//!
3//! It is here, at the bottom of the graph, because it appears in the public
4//! signatures of six crates and none of them is below the others.
5
6use core::fmt;
7
8/// A zero-based page index.
9///
10/// `PageIndex(0)` is the first page. It is **not** a count: a three-page
11/// document has `page_count() == 3` and valid indices 0, 1 and 2, and
12/// `Document::page_count` deliberately stays `u32` rather than becoming a
13/// one-past-the-end `PageIndex` — a count answers "how many", an index answers
14/// "which one", and giving them the same type would let one be passed where
15/// the other is meant, which is the whole reason this newtype exists.
16///
17/// Nor is it validated. Nothing stops `PageIndex::new(9000)` on a two-page
18/// document; what it names is checked where it is used, and the answer there
19/// is a `Result` or an `Option`. A destination that resolves to no page at all
20/// is `Option<PageIndex>` and never a sentinel.
21///
22/// `From<u32>` exists so `impl Into<PageIndex>` arguments accept a literal:
23/// `doc.page(0)` needs no wrapping at the call site.
24///
25/// ```
26/// use pdfrum_common::PageIndex;
27///
28/// let first = PageIndex::new(0);
29/// assert_eq!(first.get(), 0);
30/// assert_eq!(first.to_string(), "0");
31/// assert_eq!(PageIndex::from(4), PageIndex::new(4));
32/// assert!(PageIndex::new(1) < PageIndex::new(2));
33/// ```
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
35pub struct PageIndex(u32);
36
37impl PageIndex {
38 /// The first page.
39 pub const FIRST: Self = Self(0);
40
41 /// A page index from its number.
42 #[must_use]
43 pub const fn new(n: u32) -> Self {
44 Self(n)
45 }
46
47 /// The number back.
48 ///
49 /// The escape hatch for the arithmetic this type deliberately does not
50 /// have: no `+`, no `-`, no `Step`, because a page index plus a page index
51 /// is not a page index and the compiler should say so.
52 #[must_use]
53 pub const fn get(self) -> u32 {
54 self.0
55 }
56}
57
58impl From<u32> for PageIndex {
59 fn from(n: u32) -> Self {
60 Self(n)
61 }
62}
63
64impl From<PageIndex> for u32 {
65 fn from(index: PageIndex) -> Self {
66 index.0
67 }
68}
69
70impl fmt::Display for PageIndex {
71 /// The bare number, zero-based, as every message in this workspace already
72 /// spells it. A user-facing "page 1 of 3" is the caller's presentation
73 /// choice and is not made here.
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 fmt::Display::fmt(&self.0, f)
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::PageIndex;
82
83 #[test]
84 fn from_u32_round_trips_both_ways() {
85 for n in [0u32, 1, 2, 41, u32::MAX] {
86 let index = PageIndex::from(n);
87 assert_eq!(index.get(), n);
88 assert_eq!(u32::from(index), n);
89 assert_eq!(index, PageIndex::new(n));
90 }
91 }
92
93 #[test]
94 fn display_is_the_bare_zero_based_number() {
95 assert_eq!(PageIndex::new(0).to_string(), "0");
96 assert_eq!(PageIndex::new(41).to_string(), "41");
97 assert_eq!(format!("page {}", PageIndex::new(2)), "page 2");
98 }
99
100 #[test]
101 fn first_and_default_are_page_zero() {
102 assert_eq!(PageIndex::FIRST, PageIndex::new(0));
103 assert_eq!(PageIndex::default(), PageIndex::FIRST);
104 }
105
106 // Ordering is the underlying number's, which is what makes a page range
107 // and a sort by page mean what they say.
108 #[test]
109 fn ordering_is_the_numbers() {
110 let mut pages = [PageIndex::new(2), PageIndex::new(0), PageIndex::new(1)];
111 pages.sort_unstable();
112 assert_eq!(
113 pages,
114 [PageIndex::new(0), PageIndex::new(1), PageIndex::new(2)]
115 );
116 }
117
118 // The point of the newtype: `impl Into<PageIndex>` accepts the literal a
119 // caller would have written before it existed.
120 #[test]
121 fn a_literal_converts_the_way_an_impl_into_argument_needs() {
122 fn takes(index: impl Into<PageIndex>) -> PageIndex {
123 index.into()
124 }
125 assert_eq!(takes(0), PageIndex::FIRST);
126 assert_eq!(takes(PageIndex::new(3)), PageIndex::new(3));
127 }
128}