use std::borrow::Cow;
use std::convert::{TryFrom, TryInto};
use std::fmt::Write;
use std::fmt::{self, Display};
use std::str::FromStr;
use citationberg::{GrammarGender, NumberForm, OrdinalLookup};
use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize};
use thiserror::Error;
use unscanny::Scanner;
use super::MaybeTyped;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Numeric {
pub value: NumericValue,
pub prefix: Option<Box<String>>,
pub suffix: Option<Box<String>>,
}
impl<'de> Deserialize<'de> for Numeric {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
struct OurVisitor;
impl Visitor<'_> for OurVisitor {
type Value = Numeric;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a numeric value with optional prefix and suffix")
}
fn visit_u64<E: Error>(self, v: u64) -> Result<Self::Value, E> {
Ok(Numeric::new(v.try_into().map_err(|_| E::custom("value too large"))?))
}
fn visit_i64<E: Error>(self, v: i64) -> Result<Self::Value, E> {
Ok(Numeric::new(
v.try_into().map_err(|_| E::custom("value out of bounds"))?,
))
}
fn visit_i32<E: Error>(self, v: i32) -> Result<Self::Value, E> {
Ok(Numeric::new(v))
}
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
Self::Value::from_str(v).map_err(|e| E::custom(e.to_string()))
}
}
deserializer.deserialize_any(OurVisitor)
}
}
impl Serialize for Numeric {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self.value {
NumericValue::Number(n) if self.will_transform() => {
serializer.serialize_i32(n)
}
_ => serializer.serialize_str(&self.to_string()),
}
}
}
impl Numeric {
pub fn new(value: i32) -> Self {
Self {
value: NumericValue::Number(value),
prefix: None,
suffix: None,
}
}
pub fn from_range(range: std::ops::Range<i32>) -> Self {
Self {
value: NumericValue::Set(vec![
(range.start, Some(NumericDelimiter::Hyphen)),
(range.end, None),
]),
prefix: None,
suffix: None,
}
}
pub fn will_transform(&self) -> bool {
self.prefix.is_none() && self.suffix.is_none()
}
pub fn prefix_str(&self) -> Option<&str> {
self.prefix.as_deref().map(String::as_str)
}
pub fn suffix_str(&self) -> Option<&str> {
self.suffix.as_deref().map(String::as_str)
}
pub fn fmt_value<T>(&self, buf: &mut T, machine_readable: bool) -> std::fmt::Result
where
T: fmt::Write,
{
let format = |n: i32, buf: &mut T| -> std::fmt::Result { write!(buf, "{n}") };
match &self.value {
&NumericValue::Number(n) => format(n, buf)?,
NumericValue::Set(s) => {
for &(n, sep) in s {
format(n, buf)?;
if let Some(sep) = sep {
if machine_readable {
buf.write_char(sep.as_char())?
} else {
write!(buf, "{sep}")?
}
}
}
}
}
Ok(())
}
fn fmt_custom<T>(&self, buf: &mut T, machine_readable: bool) -> std::fmt::Result
where
T: fmt::Write,
{
if let Some(prefix) = &self.prefix {
buf.write_str(prefix)?;
}
self.fmt_value(buf, machine_readable)?;
if let Some(suffix) = &self.suffix {
buf.write_str(suffix)?;
}
Ok(())
}
pub fn with_form<T>(
&self,
buf: &mut T,
form: NumberForm,
gender: Option<GrammarGender>,
ords: &OrdinalLookup<'_>,
) -> std::fmt::Result
where
T: Write,
{
let format = |n: i32, buf: &mut T| -> std::fmt::Result {
match form {
NumberForm::Ordinal => {
write!(buf, "{}{}", n, ords.lookup(n, gender).unwrap_or_default())
}
NumberForm::LongOrdinal => match ords.lookup_long(n) {
Some(str) => buf.write_str(str),
None => {
write!(buf, "{}{}", n, ords.lookup(n, gender).unwrap_or_default())
}
},
NumberForm::Roman => match roman_numerals_rs::RomanNumeral::try_from(n) {
Ok(roman) => write!(buf, "{:x}", roman),
Err(_) => write!(buf, "{n}"),
},
NumberForm::Numeric => write!(buf, "{n}"),
}
};
match &self.value {
&NumericValue::Number(n) => format(n, buf)?,
NumericValue::Set(s) => {
for &(n, sep) in s {
format(n, buf)?;
if let Some(sep) = sep {
write!(buf, "{sep}")?
}
}
}
}
Ok(())
}
pub fn is_plural(&self, is_number_of: bool) -> bool {
match &self.value {
NumericValue::Number(n) if is_number_of => n != &1,
NumericValue::Number(_) => false,
NumericValue::Set(vec) => vec.len() != 1,
}
}
pub fn single_number(&self) -> Option<i32> {
(self.prefix.is_none() && self.suffix.is_none())
.then_some(match &self.value {
NumericValue::Number(n) => Some(*n),
_ => None,
})
.flatten()
}
pub fn nth(&self, n: usize) -> Option<i32> {
self.value.nth(n)
}
pub(crate) fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering {
self.value.into_iter().cmp(&other.value)
}
}
impl MaybeTyped<Cow<'_, Numeric>> {
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()),
}
}
}
impl FromStr for Numeric {
type Err = NumericError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let mut s = Scanner::new(value);
let prefix =
s.eat_while(|c: char| !c.is_numeric() && !c.is_whitespace() && c != '-');
let value = number(&mut s).ok_or(NumericError::NoNumber)?;
s.eat_whitespace();
let value = match s.peek() {
Some(c) if is_delimiter(c) => {
s.eat();
s.eat_until(|c: char| !is_delimiter(c));
let mut items = vec![(value, Some(NumericDelimiter::try_from(c)?))];
loop {
let num = number(&mut s).ok_or(NumericError::NoNumber)?;
s.eat_whitespace();
match NumericDelimiter::from_str(s.eat_while(is_delimiter)) {
Ok(d) => {
items.push((num, Some(d)));
}
Err(_) => {
items.push((num, None));
break;
}
}
}
NumericValue::Set(items)
}
_ => NumericValue::Number(value),
};
s.eat_whitespace();
let post = s.eat_while(|c: char| !c.is_whitespace());
if !s.after().is_empty() {
return Err(NumericError::UnexpectedCharactersAfterPostfix);
}
Ok(Self {
value,
prefix: if prefix.is_empty() {
None
} else {
Some(Box::new(prefix.to_string()))
},
suffix: if post.is_empty() { None } else { Some(Box::new(post.to_string())) },
})
}
}
impl From<i32> for Numeric {
fn from(n: i32) -> Self {
Self::new(n)
}
}
impl From<u32> for Numeric {
fn from(n: u32) -> Self {
Self::new(n as i32)
}
}
#[derive(Debug, Clone, Copy, Error, PartialEq, Eq)]
pub enum NumericError {
#[error("no number found")]
NoNumber,
#[error("unexpected characters after postfix")]
UnexpectedCharactersAfterPostfix,
#[error("not a delimiter")]
NotADelimiter,
#[error("missing delimiter")]
MissingDelimiter,
}
fn number(s: &mut Scanner) -> Option<i32> {
s.eat_whitespace();
let negative = s.eat_if('-');
let num = s.eat_while(|c: char| c.is_numeric());
if num.is_empty() {
return None;
}
num.parse::<i32>().ok().map(|n| if negative { -n } else { n })
}
impl Display for Numeric {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
self.fmt_custom(f, false)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum NumericValue {
Number(i32),
Set(Vec<(i32, Option<NumericDelimiter>)>),
}
impl NumericValue {
pub fn len(&self) -> usize {
match self {
NumericValue::Number(_) => 1,
NumericValue::Set(vec) => vec.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
fn nth(&self, n: usize) -> Option<i32> {
match self {
NumericValue::Number(val) if n == 0 => Some(*val),
NumericValue::Number(_) => None,
NumericValue::Set(vec) => vec.get(n).map(|(val, _)| *val),
}
}
}
pub struct NumIterator<'a> {
num: &'a NumericValue,
idx: usize,
}
impl Iterator for NumIterator<'_> {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
let val = self.num.nth(self.idx);
self.idx += 1;
val
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.num.len() - self.idx;
(len, Some(len))
}
}
impl ExactSizeIterator for NumIterator<'_> {
fn len(&self) -> usize {
self.size_hint().0
}
}
impl<'a> IntoIterator for &'a NumericValue {
type Item = i32;
type IntoIter = NumIterator<'a>;
fn into_iter(self) -> Self::IntoIter {
NumIterator { num: self, idx: 0 }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NumericDelimiter {
Comma,
Ampersand,
Hyphen,
}
impl NumericDelimiter {
pub fn as_char(&self) -> char {
match self {
NumericDelimiter::Comma => ',',
NumericDelimiter::Ampersand => '&',
NumericDelimiter::Hyphen => '-',
}
}
}
impl std::fmt::Display for NumericDelimiter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NumericDelimiter::Comma => f.write_str(", "),
NumericDelimiter::Ampersand => f.write_str(" & "),
NumericDelimiter::Hyphen => f.write_char('–'),
}
}
}
fn is_delimiter(c: char) -> bool {
c == ',' || c == '&' || c == '-' || c == '–'
}
impl FromStr for NumericDelimiter {
type Err = NumericError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let first_char = s.chars().next().ok_or(NumericError::MissingDelimiter)?;
if first_char != '-' && s.len() > first_char.len_utf8() {
return Err(NumericError::NotADelimiter);
}
Self::try_from(first_char)
}
}
impl TryFrom<char> for NumericDelimiter {
type Error = NumericError;
fn try_from(c: char) -> Result<Self, Self::Error> {
match c {
',' => Ok(NumericDelimiter::Comma),
'&' => Ok(NumericDelimiter::Ampersand),
'-' | '–' => Ok(NumericDelimiter::Hyphen),
_ => Err(NumericError::NotADelimiter),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_mixed_range() {
let s = "34,37--39";
let n: Numeric = s.parse().unwrap();
assert_eq!(
n.value,
NumericValue::Set(vec![
(34, Some(NumericDelimiter::Comma)),
(37, Some(NumericDelimiter::Hyphen)),
(39, None)
])
);
}
}