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 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct PageRanges {
22 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 pub fn new(ranges: Vec<PageRangesPart>) -> Self {
45 Self { ranges }
46 }
47
48 pub fn first(&self) -> Option<&Numeric> {
50 self.ranges.iter().find_map(PageRangesPart::start)
51 }
52
53 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 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 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
146pub enum PageRangesPart {
147 Ampersand,
149 Comma,
151 EscapedRange(Numeric, Numeric),
153 SinglePage(Numeric),
155 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 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 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 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#[derive(Debug, Clone, Copy, Error)]
276pub enum PageRangesPartErr {
277 #[error("page range string malformed")]
279 Malformed,
280 #[error("page range is empty")]
282 Empty,
283 #[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 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 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
338pub(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
348pub(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 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}