use std::{cmp::Ordering, fmt::Display, num::TryFromIntError, str::FromStr};
use crate::{MaybeTyped, Numeric, NumericError};
use super::{custom_deserialize, serialize_display};
use serde::{Deserialize, Serialize};
use thiserror::Error;
impl MaybeTyped<PageRanges> {
pub(crate) fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(MaybeTyped::Typed(a), MaybeTyped::Typed(b)) => a.csl_cmp(b),
_ => self.to_string().cmp(&other.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PageRanges {
pub ranges: Vec<PageRangesPart>,
}
custom_deserialize!(
PageRanges where "pages, page ranges, ampersands, and commas"
fn visit_i32<E: serde::de::Error>(self, v: i32) -> Result<Self::Value, E> {
Ok(PageRanges::from(v))
}
fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<Self::Value, E> {
PageRanges::try_from(v).map_err(|_| E::custom("value too large"))
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
PageRanges::try_from(v).map_err(|_| E::custom("value out of bounds"))
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
PageRanges::try_from(v).map_err(|_| E::custom("value too large"))
}
);
impl PageRanges {
pub fn new(ranges: Vec<PageRangesPart>) -> Self {
Self { ranges }
}
pub fn first(&self) -> Option<&Numeric> {
self.ranges.iter().find_map(PageRangesPart::start)
}
pub(crate) fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering {
#[derive(PartialEq, Eq)]
struct OrderablePageRangesPart<'a>(&'a PageRangesPart);
impl Ord for OrderablePageRangesPart<'_> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.csl_cmp(other.0)
}
}
impl PartialOrd for OrderablePageRangesPart<'_> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
self.ranges
.iter()
.map(OrderablePageRangesPart)
.cmp(other.ranges.iter().map(OrderablePageRangesPart))
}
pub fn is_plural(&self) -> bool {
let mut count = 0;
for range in &self.ranges {
match range {
PageRangesPart::SinglePage(_) => count += 1,
PageRangesPart::Range(s, e) | PageRangesPart::EscapedRange(s, e) => {
if s != e {
return true;
}
count += 1
}
_ => {}
}
}
count > 1
}
}
impl From<i32> for PageRanges {
fn from(value: i32) -> Self {
Self { ranges: vec![value.into()] }
}
}
impl TryFrom<u32> for PageRanges {
type Error = TryFromIntError;
fn try_from(value: u32) -> Result<Self, Self::Error> {
Ok(Self { ranges: vec![value.try_into()?] })
}
}
impl TryFrom<i64> for PageRanges {
type Error = TryFromIntError;
fn try_from(value: i64) -> Result<Self, Self::Error> {
Ok(Self { ranges: vec![value.try_into()?] })
}
}
impl TryFrom<u64> for PageRanges {
type Error = TryFromIntError;
fn try_from(value: u64) -> Result<Self, Self::Error> {
Ok(Self { ranges: vec![value.try_into()?] })
}
}
impl Display for PageRanges {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.ranges.iter().try_for_each(|r| r.fmt(f))
}
}
impl FromStr for PageRanges {
type Err = PageRangesPartErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self {
ranges: group_by(s, |c, d| !(c == ',' || c == '&' || d == ',' || d == '&'))
.map(PageRangesPart::from_str)
.collect::<Result<_, _>>()?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PageRangesPart {
Ampersand,
Comma,
EscapedRange(Numeric, Numeric),
SinglePage(Numeric),
Range(Numeric, Numeric),
}
custom_deserialize!(
PageRangesPart where "a page, a page range, or a separator"
fn visit_i32<E: serde::de::Error>(self, v: i32) -> Result<Self::Value, E> {
Ok(PageRangesPart::from(v))
}
fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<Self::Value, E> {
PageRangesPart::try_from(v).map_err(|_| E::custom("value too large"))
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
PageRangesPart::try_from(v).map_err(|_| E::custom("value out of bounds"))
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
PageRangesPart::try_from(v).map_err(|_| E::custom("value too large"))
}
);
impl PageRangesPart {
pub fn start(&self) -> Option<&Numeric> {
match self {
Self::EscapedRange(s, _) => Some(s),
Self::SinglePage(s) => Some(s),
Self::Range(s, _) => Some(s),
_ => None,
}
}
pub fn end(&self) -> Option<&Numeric> {
match self {
Self::EscapedRange(_, e) => Some(e),
Self::Range(_, e) => Some(e),
Self::SinglePage(_) => None,
_ => None,
}
}
pub(crate) fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(Self::Ampersand, Self::Ampersand) => Ordering::Equal,
(Self::Ampersand, _) => Ordering::Less,
(_, Self::Ampersand) => Ordering::Greater,
(Self::Comma, Self::Comma) => Ordering::Equal,
(Self::Comma, _) => Ordering::Less,
(_, Self::Comma) => Ordering::Greater,
(Self::SinglePage(n1), Self::SinglePage(n2)) => n1.csl_cmp(n2),
(Self::SinglePage(_), _) => Ordering::Less,
(_, Self::SinglePage(_)) => Ordering::Greater,
(Self::EscapedRange(s1, e1), Self::EscapedRange(s2, e2)) => {
let ord = s1.csl_cmp(s2);
if ord != Ordering::Equal {
return ord;
}
e1.csl_cmp(e2)
}
(Self::EscapedRange(_, _), _) => Ordering::Less,
(_, Self::EscapedRange(_, _)) => Ordering::Greater,
(Self::Range(s1, e1), Self::Range(s2, e2)) => {
let ord = s1.csl_cmp(s2);
if ord != Ordering::Equal {
return ord;
}
e1.csl_cmp(e2)
}
}
}
}
impl From<i32> for PageRangesPart {
fn from(value: i32) -> Self {
Self::SinglePage(value.into())
}
}
impl TryFrom<u32> for PageRangesPart {
type Error = TryFromIntError;
fn try_from(value: u32) -> Result<Self, Self::Error> {
let value: i32 = value.try_into()?;
Ok(Self::SinglePage(value.into()))
}
}
impl TryFrom<i64> for PageRangesPart {
type Error = TryFromIntError;
fn try_from(value: i64) -> Result<Self, Self::Error> {
let value: i32 = value.try_into()?;
Ok(Self::SinglePage(value.into()))
}
}
impl TryFrom<u64> for PageRangesPart {
type Error = TryFromIntError;
fn try_from(value: u64) -> Result<Self, Self::Error> {
let value: i32 = value.try_into()?;
Ok(Self::SinglePage(value.into()))
}
}
impl Display for PageRangesPart {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
PageRangesPart::Ampersand => "&",
PageRangesPart::Comma => ", ",
PageRangesPart::EscapedRange(s, e) => return write!(f, "{s}-{e}"),
PageRangesPart::SinglePage(s) => return write!(f, "{s}"),
PageRangesPart::Range(s, e) => return write!(f, "{s}-{e}"),
};
Display::fmt(s, f)
}
}
#[derive(Debug, Clone, Copy, Error)]
pub enum PageRangesPartErr {
#[error("page range string malformed")]
Malformed,
#[error("page range is empty")]
Empty,
#[error("page range contained invalid numeric value")]
NumericErr(#[from] NumericError),
}
impl FromStr for PageRangesPart {
type Err = PageRangesPartErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
return Err(PageRangesPartErr::Empty);
}
let p = if s == "&" {
Self::Ampersand
} else if s == "," {
Self::Comma
} else if s.contains("\\-") {
let mut parts = s.split("\\-").map(str::trim);
let start = parts.next().ok_or(PageRangesPartErr::Empty)?;
let end = parts.next().ok_or(PageRangesPartErr::Empty)?;
let r = Self::EscapedRange(parse_number(start)?, parse_number(end)?);
if parts.next().is_some() {
return Err(PageRangesPartErr::Malformed);
}
r
} else {
let mut parts = s.split(['-', '–']).map(str::trim);
let r = match (parts.next(), parts.next()) {
(None, None) => unreachable!(),
(Some(start), None) => Self::SinglePage(parse_number(start)?),
(Some(start), Some(end)) => {
Self::Range(parse_number(start)?, parse_number(end)?)
}
_ => unreachable!(),
};
if parts.next().is_some() {
return Err(PageRangesPartErr::Malformed);
}
r
};
Ok(p)
}
}
serialize_display!(PageRanges);
fn parse_number(s: &str) -> Result<Numeric, NumericError> {
Numeric::from_str(s)
}
pub(crate) fn group_by<F>(s: &str, pred: F) -> GroupBy<'_, F>
where
F: FnMut(char, char) -> bool,
{
GroupBy::new(s, pred)
}
pub(crate) struct GroupBy<'a, P> {
string: &'a str,
predicate: P,
}
impl<'a, P> GroupBy<'a, P> {
pub(crate) fn new(string: &'a str, predicate: P) -> Self {
GroupBy { string, predicate }
}
}
impl<'a, P> Iterator for GroupBy<'a, P>
where
P: FnMut(char, char) -> bool,
{
type Item = &'a str;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
if let Some(first_char) = self.string.chars().next() {
let mut len = first_char.len_utf8();
for (c, d) in self.string.chars().zip(self.string.chars().skip(1)) {
if (self.predicate)(c, d) {
len += d.len_utf8();
} else {
break;
}
}
let (head, tail) = self.string.split_at(len);
self.string = tail;
Some(head)
} else {
None
}
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.string.chars().size_hint()
}
}
#[cfg(test)]
mod test {
#[test]
fn group_by() {
fn group(s: &str) -> Vec<&'_ str> {
super::group_by(s, |c, d| !(c == ',' || c == '&' || d == ',' || d == '&'))
.collect()
}
assert_eq!(["a"], group("a").as_slice());
assert_eq!(["a", ","], group("a,").as_slice());
assert_eq!([",", "a"], group(",a").as_slice());
assert_eq!([",", "a", ","], group(",a,").as_slice());
assert_eq!(["a", ",", "b"], group("a,b").as_slice());
assert_eq!(["a-"], group("a-").as_slice());
assert_eq!(["a–"], group("a–").as_slice());
assert_eq!(["–a"], group("–a").as_slice());
assert_eq!(["–a", ","], group("–a,").as_slice());
assert_eq!(["a–", ",", "–b"], group("a–,–b").as_slice());
}
}