hayagriva/types/
page.rs

1use std::{cmp::Ordering, fmt::Display, num::TryFromIntError, str::FromStr};
2
3use crate::{MaybeTyped, Numeric, NumericError};
4
5use super::{custom_deserialize, serialize_display};
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9impl MaybeTyped<PageRanges> {
10    /// Order the values according to CSL rules.
11    pub(crate) fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering {
12        match (self, other) {
13            (MaybeTyped::Typed(a), MaybeTyped::Typed(b)) => a.csl_cmp(b),
14            _ => self.to_string().cmp(&other.to_string()),
15        }
16    }
17}
18
19/// Ranges of page numbers, e.g., `1-4, 5 & 6`.
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct PageRanges {
22    /// The given ranges.
23    pub ranges: Vec<PageRangesPart>,
24}
25
26custom_deserialize!(
27    PageRanges where "pages, page ranges, ampersands, and commas"
28    fn visit_i32<E: serde::de::Error>(self, v: i32) -> Result<Self::Value, E> {
29        Ok(PageRanges::from(v))
30    }
31    fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<Self::Value, E> {
32        PageRanges::try_from(v).map_err(|_| E::custom("value too large"))
33    }
34    fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
35        PageRanges::try_from(v).map_err(|_| E::custom("value out of bounds"))
36    }
37    fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
38        PageRanges::try_from(v).map_err(|_| E::custom("value too large"))
39    }
40);
41
42impl PageRanges {
43    /// Create a new `PageRanges` struct.
44    pub fn new(ranges: Vec<PageRangesPart>) -> Self {
45        Self { ranges }
46    }
47
48    /// Get the first page of the first range.
49    pub fn first(&self) -> Option<&Numeric> {
50        self.ranges.iter().find_map(PageRangesPart::start)
51    }
52
53    /// Order the values according to CSL rules.
54    pub(crate) fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering {
55        #[derive(PartialEq, Eq)]
56        struct OrderablePageRangesPart<'a>(&'a PageRangesPart);
57
58        impl Ord for OrderablePageRangesPart<'_> {
59            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
60                self.0.csl_cmp(other.0)
61            }
62        }
63
64        impl PartialOrd for OrderablePageRangesPart<'_> {
65            fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
66                Some(self.cmp(other))
67            }
68        }
69
70        self.ranges
71            .iter()
72            .map(OrderablePageRangesPart)
73            .cmp(other.ranges.iter().map(OrderablePageRangesPart))
74    }
75
76    /// Whether to pluralize the `pages` term, when used with this page range.
77    pub fn is_plural(&self) -> bool {
78        let mut count = 0;
79        for range in &self.ranges {
80            match range {
81                PageRangesPart::SinglePage(_) => count += 1,
82                PageRangesPart::Range(s, e) | PageRangesPart::EscapedRange(s, e) => {
83                    if s != e {
84                        return true;
85                    }
86                    count += 1
87                }
88                _ => {}
89            }
90        }
91        count > 1
92    }
93}
94
95impl From<i32> for PageRanges {
96    fn from(value: i32) -> Self {
97        Self { ranges: vec![value.into()] }
98    }
99}
100
101impl TryFrom<u32> for PageRanges {
102    type Error = TryFromIntError;
103
104    fn try_from(value: u32) -> Result<Self, Self::Error> {
105        Ok(Self { ranges: vec![value.try_into()?] })
106    }
107}
108
109impl TryFrom<i64> for PageRanges {
110    type Error = TryFromIntError;
111
112    fn try_from(value: i64) -> Result<Self, Self::Error> {
113        Ok(Self { ranges: vec![value.try_into()?] })
114    }
115}
116
117impl TryFrom<u64> for PageRanges {
118    type Error = TryFromIntError;
119
120    fn try_from(value: u64) -> Result<Self, Self::Error> {
121        Ok(Self { ranges: vec![value.try_into()?] })
122    }
123}
124
125impl Display for PageRanges {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        self.ranges.iter().try_for_each(|r| r.fmt(f))
128    }
129}
130
131impl FromStr for PageRanges {
132    type Err = PageRangesPartErr;
133
134    fn from_str(s: &str) -> Result<Self, Self::Err> {
135        // Split input into different ranges separated by `&` or `,`
136        Ok(Self {
137            ranges: group_by(s, |c, d| !(c == ',' || c == '&' || d == ',' || d == '&'))
138                .map(PageRangesPart::from_str)
139                .collect::<Result<_, _>>()?,
140        })
141    }
142}
143
144/// Parts of the page ranges.
145#[derive(Debug, Clone, PartialEq, Eq, Hash)]
146pub enum PageRangesPart {
147    /// An and, i.e, `&`.
148    Ampersand,
149    /// A comma, i.e., `,`.
150    Comma,
151    /// An escaped range with start and end, e.g., `1\-4`.
152    EscapedRange(Numeric, Numeric),
153    /// A single page, e.g., `5`.
154    SinglePage(Numeric),
155    /// A full range, e.g., `1n8--1n14`.
156    Range(Numeric, Numeric),
157}
158
159custom_deserialize!(
160    PageRangesPart where "a page, a page range, or a separator"
161    fn visit_i32<E: serde::de::Error>(self, v: i32) -> Result<Self::Value, E> {
162        Ok(PageRangesPart::from(v))
163    }
164    fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<Self::Value, E> {
165        PageRangesPart::try_from(v).map_err(|_| E::custom("value too large"))
166    }
167    fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
168        PageRangesPart::try_from(v).map_err(|_| E::custom("value out of bounds"))
169    }
170    fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
171        PageRangesPart::try_from(v).map_err(|_| E::custom("value too large"))
172    }
173);
174
175impl PageRangesPart {
176    /// The start of a range, if any.
177    pub fn start(&self) -> Option<&Numeric> {
178        match self {
179            Self::EscapedRange(s, _) => Some(s),
180            Self::SinglePage(s) => Some(s),
181            Self::Range(s, _) => Some(s),
182            _ => None,
183        }
184    }
185
186    /// The end of a range, if any.
187    pub fn end(&self) -> Option<&Numeric> {
188        match self {
189            Self::EscapedRange(_, e) => Some(e),
190            Self::Range(_, e) => Some(e),
191            Self::SinglePage(_) => None,
192            _ => None,
193        }
194    }
195
196    /// Order the values according to CSL rules.
197    pub(crate) fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering {
198        match (self, other) {
199            (Self::Ampersand, Self::Ampersand) => Ordering::Equal,
200            (Self::Ampersand, _) => Ordering::Less,
201            (_, Self::Ampersand) => Ordering::Greater,
202            (Self::Comma, Self::Comma) => Ordering::Equal,
203            (Self::Comma, _) => Ordering::Less,
204            (_, Self::Comma) => Ordering::Greater,
205            (Self::SinglePage(n1), Self::SinglePage(n2)) => n1.csl_cmp(n2),
206            (Self::SinglePage(_), _) => Ordering::Less,
207            (_, Self::SinglePage(_)) => Ordering::Greater,
208            (Self::EscapedRange(s1, e1), Self::EscapedRange(s2, e2)) => {
209                let ord = s1.csl_cmp(s2);
210                if ord != Ordering::Equal {
211                    return ord;
212                }
213                e1.csl_cmp(e2)
214            }
215            (Self::EscapedRange(_, _), _) => Ordering::Less,
216            (_, Self::EscapedRange(_, _)) => Ordering::Greater,
217            (Self::Range(s1, e1), Self::Range(s2, e2)) => {
218                let ord = s1.csl_cmp(s2);
219                if ord != Ordering::Equal {
220                    return ord;
221                }
222                e1.csl_cmp(e2)
223            }
224        }
225    }
226}
227
228impl From<i32> for PageRangesPart {
229    fn from(value: i32) -> Self {
230        Self::SinglePage(value.into())
231    }
232}
233
234impl TryFrom<u32> for PageRangesPart {
235    type Error = TryFromIntError;
236
237    fn try_from(value: u32) -> Result<Self, Self::Error> {
238        let value: i32 = value.try_into()?;
239        Ok(Self::SinglePage(value.into()))
240    }
241}
242
243impl TryFrom<i64> for PageRangesPart {
244    type Error = TryFromIntError;
245
246    fn try_from(value: i64) -> Result<Self, Self::Error> {
247        let value: i32 = value.try_into()?;
248        Ok(Self::SinglePage(value.into()))
249    }
250}
251
252impl TryFrom<u64> for PageRangesPart {
253    type Error = TryFromIntError;
254
255    fn try_from(value: u64) -> Result<Self, Self::Error> {
256        let value: i32 = value.try_into()?;
257        Ok(Self::SinglePage(value.into()))
258    }
259}
260
261impl Display for PageRangesPart {
262    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263        let s = match self {
264            PageRangesPart::Ampersand => "&",
265            PageRangesPart::Comma => ", ",
266            PageRangesPart::EscapedRange(s, e) => return write!(f, "{s}-{e}"),
267            PageRangesPart::SinglePage(s) => return write!(f, "{s}"),
268            PageRangesPart::Range(s, e) => return write!(f, "{s}-{e}"),
269        };
270        Display::fmt(s, f)
271    }
272}
273
274/// Parsing error for page ranges.
275#[derive(Debug, Clone, Copy, Error)]
276pub enum PageRangesPartErr {
277    /// The string is malformed.
278    #[error("page range string malformed")]
279    Malformed,
280    /// The string is empty.
281    #[error("page range is empty")]
282    Empty,
283    /// An error from parsing a numeric value.
284    #[error("page range contained invalid numeric value")]
285    NumericErr(#[from] NumericError),
286}
287
288impl FromStr for PageRangesPart {
289    type Err = PageRangesPartErr;
290
291    fn from_str(s: &str) -> Result<Self, Self::Err> {
292        let s = s.trim();
293        if s.is_empty() {
294            return Err(PageRangesPartErr::Empty);
295        }
296        let p = if s == "&" {
297            Self::Ampersand
298        } else if s == "," {
299            Self::Comma
300        } else if s.contains("\\-") {
301            // If `-` chars are escaped, write `-`.
302            let mut parts = s.split("\\-").map(str::trim);
303
304            let start = parts.next().ok_or(PageRangesPartErr::Empty)?;
305            let end = parts.next().ok_or(PageRangesPartErr::Empty)?;
306
307            let r = Self::EscapedRange(parse_number(start)?, parse_number(end)?);
308            if parts.next().is_some() {
309                return Err(PageRangesPartErr::Malformed);
310            }
311            r
312        } else {
313            // Otherwise, split into the two halves of the dash.
314            let mut parts = s.split(['-', '–']).map(str::trim);
315            let r = match (parts.next(), parts.next()) {
316                (None, None) => unreachable!(),
317                (Some(start), None) => Self::SinglePage(parse_number(start)?),
318                (Some(start), Some(end)) => {
319                    Self::Range(parse_number(start)?, parse_number(end)?)
320                }
321                _ => unreachable!(),
322            };
323            if parts.next().is_some() {
324                return Err(PageRangesPartErr::Malformed);
325            }
326            r
327        };
328        Ok(p)
329    }
330}
331
332serialize_display!(PageRanges);
333
334fn parse_number(s: &str) -> Result<Numeric, NumericError> {
335    Numeric::from_str(s)
336}
337
338/// Split `s` into maximal chunks such that two successive chars satisfy `pred`.
339///
340/// Returns an iterator over these chunks.
341pub(crate) fn group_by<F>(s: &str, pred: F) -> GroupBy<'_, F>
342where
343    F: FnMut(char, char) -> bool,
344{
345    GroupBy::new(s, pred)
346}
347
348/// An iterator over string slice in (non-overlapping) chunks separated by a predicate.
349///
350/// Adapted from the nightly std.
351pub(crate) struct GroupBy<'a, P> {
352    string: &'a str,
353    predicate: P,
354}
355
356impl<'a, P> GroupBy<'a, P> {
357    pub(crate) fn new(string: &'a str, predicate: P) -> Self {
358        GroupBy { string, predicate }
359    }
360}
361
362impl<'a, P> Iterator for GroupBy<'a, P>
363where
364    P: FnMut(char, char) -> bool,
365{
366    type Item = &'a str;
367
368    #[inline]
369    fn next(&mut self) -> Option<Self::Item> {
370        if let Some(first_char) = self.string.chars().next() {
371            let mut len = first_char.len_utf8();
372            for (c, d) in self.string.chars().zip(self.string.chars().skip(1)) {
373                if (self.predicate)(c, d) {
374                    len += d.len_utf8();
375                } else {
376                    break;
377                }
378            }
379            let (head, tail) = self.string.split_at(len);
380            self.string = tail;
381            Some(head)
382        } else {
383            None
384        }
385    }
386
387    #[inline]
388    fn size_hint(&self) -> (usize, Option<usize>) {
389        self.string.chars().size_hint()
390    }
391}
392
393#[cfg(test)]
394mod test {
395    #[test]
396    fn group_by() {
397        fn group(s: &str) -> Vec<&'_ str> {
398            super::group_by(s, |c, d| !(c == ',' || c == '&' || d == ',' || d == '&'))
399                .collect()
400        }
401        assert_eq!(["a"], group("a").as_slice());
402        assert_eq!(["a", ","], group("a,").as_slice());
403        assert_eq!([",", "a"], group(",a").as_slice());
404        assert_eq!([",", "a", ","], group(",a,").as_slice());
405        assert_eq!(["a", ",", "b"], group("a,b").as_slice());
406        assert_eq!(["a-"], group("a-").as_slice());
407        // characters that are longer than 1 byte
408        assert_eq!(["a–"], group("a–").as_slice());
409        assert_eq!(["–a"], group("–a").as_slice());
410        assert_eq!(["–a", ","], group("–a,").as_slice());
411        assert_eq!(["a–", ",", "–b"], group("a–,–b").as_slice());
412    }
413}