Skip to main content

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}