use std::convert::TryFrom;
use std::fmt::{Display, Formatter};
use std::ops::Range;
use std::ops::{Add, Sub};
use std::{
cmp::{Ordering, PartialOrd},
str::FromStr,
};
use chrono::{Datelike, NaiveDate};
use lazy_static::lazy_static;
use regex::Regex;
use strum::{Display, EnumString};
use thiserror::Error;
use unicode_segmentation::UnicodeSegmentation;
use url::{Host, Url};
use super::{Entry, Value};
use crate::lang::Case;
#[rustfmt::skip]
lazy_static! {
static ref RANGE_REGEX: Regex = Regex::new(r"^(?P<s>(\+|-)?\s*\d+)(\s*-+\s*(?P<e>(\+|-)?\s*\d+))?").unwrap();
static ref DURATION_REGEX: Regex = Regex::new(r"^(((?P<d>\d+)\s*:\s*)?(?P<h>\d{2,})\s*:\s*)?(?P<m>\d{2,})\s*:\s*(?P<s>\d{2})(\s*,\s*(?P<ms>\d+))?").unwrap();
static ref DURATION_RANGE_REGEX: Regex = Regex::new(r"^(?P<s>((\d+\s*:\s*)?\d{2,}\s*:\s*)?\d{2,}\s*:\s*\d{2}(\s*,\s*\d+)?)(\s*-+\s*(?P<e>((\d+\s*:\s*)?\d{2,}\s*:\s*)?\d{2,}\s*:\s*\d{2}(\s*,\s*\d+)?))?").unwrap();
static ref MONTH_REGEX: Regex = Regex::new(r"^(?P<y>(\+|-)?\s*\d{4})\s*-\s*(?P<m>\d{2})").unwrap();
static ref YEAR_REGEX: Regex = Regex::new(r"^(?P<y>(\+|-)?\s*\d{4})").unwrap();
}
#[derive(Copy, Clone, Display, Debug, EnumString, PartialEq, Eq)]
#[non_exhaustive]
#[strum(serialize_all = "lowercase")]
pub enum EntryType {
Article,
Chapter,
Entry,
Anthos,
Report,
Thesis,
Web,
Scene,
Artwork,
Patent,
Case,
Newspaper,
Legislation,
Manuscript,
Tweet,
Misc,
Periodical,
Proceedings,
Book,
Blog,
Reference,
Conference,
Anthology,
Repository,
Thread,
Video,
Audio,
Exhibition,
}
impl Entry {
pub(crate) fn twitter_handle(&self, user_index: usize) -> Option<String> {
if self.entry_type != EntryType::Tweet {
return None;
}
let authors = self.authors().unwrap_or_default();
if user_index > 0 && user_index >= authors.len() {
return None;
}
if let Some(alias) = &authors[user_index].alias {
return if alias.starts_with('@') {
Some(alias.clone())
} else {
Some(format!("@{}", alias))
};
}
if user_index == 0 {
if let Some(url) = self.url().map(|u| &u.value) {
if url.host() != Some(Host::Domain("twitter.com")) {
return None;
}
if let Some(handle) = url.path_segments().and_then(|mut c| c.next()) {
return Some(format!("@{}", handle));
}
}
}
None
}
}
impl EntryType {
pub(crate) fn default_parent(&self) -> Self {
match self {
Self::Article => Self::Periodical,
Self::Chapter => Self::Book,
Self::Entry => Self::Reference,
Self::Anthos => Self::Anthology,
Self::Web => Self::Web,
Self::Scene => Self::Video,
Self::Artwork => Self::Exhibition,
Self::Legislation => Self::Anthology,
Self::Tweet => Self::Tweet,
Self::Video => Self::Video,
Self::Audio => Self::Audio,
_ => Self::Misc,
}
}
}
#[derive(Clone, Debug, Display, EnumString, PartialEq, Eq)]
#[non_exhaustive]
#[strum(serialize_all = "kebab_case")]
pub enum PersonRole {
Translator,
Afterword,
Foreword,
Introduction,
Annotator,
Commentator,
Holder,
Compiler,
Founder,
Collaborator,
Organizer,
CastMember,
Composer,
Producer,
ExecutiveProducer,
Writer,
Cinematography,
Director,
Illustrator,
Narrator,
#[strum(disabled)]
Unknown(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Person {
pub name: String,
pub given_name: Option<String>,
pub prefix: Option<String>,
pub suffix: Option<String>,
pub alias: Option<String>,
}
#[derive(Clone, Debug, Error)]
pub enum PersonError {
#[error("too many parts")]
TooManyParts,
#[error("part list is empty")]
Empty,
}
impl Person {
pub fn from_strings(parts: &[&str]) -> Result<Self, PersonError> {
if parts.is_empty() {
return Err(PersonError::Empty);
} else if parts.len() > 3 {
return Err(PersonError::TooManyParts);
}
let parts: Vec<&str> = parts.iter().map(|s| s.trim()).collect();
let last_pre = parts[0];
let given_name =
if parts.len() > 1 { Some(parts.last().unwrap().to_string()) } else { None };
let suffix = if parts.len() > 2 { Some(parts[1].to_string()) } else { None };
let mut word_start = true;
let mut last_lower_case_end: i32 = -1;
let mut is_lowercase = false;
let mut last_word_start = 0;
let mut has_seen_uppercase_words = false;
for (index, c) in last_pre.chars().enumerate() {
if c.is_whitespace() {
word_start = true;
continue;
}
if word_start {
last_word_start = index;
if c.is_lowercase() {
is_lowercase = true;
} else {
is_lowercase = false;
has_seen_uppercase_words = true;
}
}
if is_lowercase {
last_lower_case_end = index as i32;
}
word_start = false;
}
let mut name = String::new();
let mut prefix = String::new();
for (index, c) in last_pre.chars().enumerate() {
if (index as i32 <= last_lower_case_end && has_seen_uppercase_words)
|| (!has_seen_uppercase_words && index < last_word_start)
{
prefix.push(c);
} else if has_seen_uppercase_words || index >= last_word_start {
name.push(c);
}
}
let prefix = if prefix.is_empty() { None } else { Some(prefix) };
if prefix.is_some() {
name = name.trim_start().to_string();
}
Ok(Person { name, given_name, prefix, suffix, alias: None })
}
pub fn initials(&self, delimiter: Option<&str>) -> Option<String> {
if let Some(gn) = &self.given_name {
let mut collect = true;
let mut letters = vec![];
let mut seps = vec![];
for (_, gr) in gn.grapheme_indices(true) {
if let Some(c) = gr.chars().next() {
if c.is_whitespace() || c == '-' {
collect = true;
seps.push(c);
continue;
}
}
if collect {
letters.push(gr);
collect = false;
}
}
let mut res = String::new();
for (i, e) in letters.into_iter().enumerate() {
if i != 0 {
res.push(seps[i - 1]);
}
res += e;
if let Some(delimiter) = delimiter {
res += delimiter;
}
}
Some(res)
} else {
None
}
}
pub fn name_first(&self, initials: bool, prefix_given_name: bool) -> String {
let mut res = if !prefix_given_name {
if let Some(prefix) = &self.prefix {
format!("{} {}", prefix, self.name)
} else {
self.name.clone()
}
} else {
self.name.clone()
};
if initials {
if let Some(initials) = self.initials(Some(".")) {
res += ", ";
res += &initials;
}
} else if let Some(given_name) = &self.given_name {
res += ", ";
res += given_name;
}
if prefix_given_name {
if let Some(prefix) = &self.prefix {
if self.given_name.is_some() {
res.push(' ');
}
res += prefix;
}
}
if let Some(suffix) = &self.suffix {
res += ", ";
res += suffix;
}
res
}
pub fn given_first(&self, initials: bool) -> String {
let mut res = if initials {
if let Some(initials) = self.initials(Some(".")) {
format!("{} ", initials)
} else {
String::new()
}
} else if let Some(given_name) = self.given_name.clone() {
format!("{} ", given_name)
} else {
String::new()
};
if let Some(prefix) = &self.prefix {
res += prefix;
res += " ";
}
res += &self.name;
if let Some(suffix) = &self.suffix {
res += " ";
res += suffix;
}
res
}
}
impl Ord for Person {
fn cmp(&self, other: &Self) -> Ordering {
self.name
.cmp(&other.name)
.then(self.given_name.cmp(&other.given_name))
.then(self.suffix.cmp(&other.suffix))
.then(self.prefix.cmp(&other.prefix))
}
}
impl PartialOrd for Person {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NumOrStr {
Number(i64),
Str(String),
}
impl Display for NumOrStr {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
Self::Number(i) => write!(f, "{}", i),
Self::Str(s) => write!(f, "{}", s),
}
}
}
impl From<NumOrStr> for String {
fn from(num: NumOrStr) -> Self {
num.to_string()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Date {
pub year: i32,
pub month: Option<u8>,
pub day: Option<u8>,
}
#[derive(Clone, Debug, Error)]
pub enum DateError {
#[error("date format unknown")]
UnknownFormat,
#[error("month not in interval 1-12")]
MonthOutOfBounds,
}
impl FromStr for Date {
type Err = DateError;
fn from_str(source: &str) -> Result<Self, Self::Err> {
let mut source = source.to_string();
source.retain(|f| !f.is_whitespace());
let full_date = source.parse::<NaiveDate>();
if let Ok(ndate) = full_date {
Ok(Self {
year: ndate.year(),
month: Some(ndate.month0() as u8),
day: Some(ndate.day0() as u8),
})
} else if let Some(captures) = MONTH_REGEX.captures(&source) {
let month = (captures.name("m").unwrap()).as_str().parse::<u8>().unwrap() - 1;
if month > 11 {
Err(DateError::MonthOutOfBounds)
} else {
Ok(Self {
year: (captures.name("y").unwrap()).as_str().parse().unwrap(),
month: Some(month),
day: None,
})
}
} else if let Some(captures) = YEAR_REGEX.captures(&source) {
Ok(Self {
year: (captures.name("y").unwrap()).as_str().parse().unwrap(),
month: None,
day: None,
})
} else {
Err(DateError::UnknownFormat)
}
}
}
impl Date {
pub fn from_year(year: i32) -> Self {
Self { year, month: None, day: None }
}
pub fn display_year(&self) -> String {
self.display_year_opt(true, false, false, false)
}
pub fn display_year_opt(
&self,
secular: bool,
periods: bool,
designate_positive: bool,
ad_prefix: bool,
) -> String {
let np_postfix = match (secular, periods) {
(true, false) => "BCE",
(true, true) => "B.C.E.",
(false, false) => "BC",
(false, true) => "B.C.",
};
let positive_dn = match (periods, ad_prefix) {
(true, false) => "C.E.",
(false, false) => "CE",
(true, true) => "AD",
(false, true) => "A.D.",
};
if self.year > 0 {
if designate_positive && ad_prefix {
format!("{} {}", positive_dn, self.year)
} else if designate_positive && !ad_prefix {
format!("{} {}", self.year, positive_dn)
} else {
self.year.to_string()
}
} else {
format!("{} {}", -(self.year as i64 - 1), np_postfix)
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FmtString {
pub value: String,
pub(crate) title_case: Option<String>,
pub(crate) sentence_case: Option<String>,
pub(crate) verbatim: bool,
}
impl FmtString {
pub fn new(value: impl Into<String>) -> Self {
Self {
value: value.into(),
title_case: None,
sentence_case: None,
verbatim: false,
}
}
pub fn verbatim(self, verbatim: bool) -> Self {
Self { verbatim, ..self }
}
pub fn title_case(self, title_case: impl Into<String>) -> Self {
Self { title_case: Some(title_case.into()), ..self }
}
pub fn sentence_case(self, sentence_case: impl Into<String>) -> Self {
Self { sentence_case: Some(sentence_case.into()), ..self }
}
pub fn format_title_case(&self, title: &dyn Case) -> String {
self.title_case.clone().unwrap_or_else(|| title.apply(&self.value))
}
pub fn format_sentence_case(&self, sentence: &dyn Case) -> String {
self.sentence_case
.clone()
.unwrap_or_else(|| sentence.apply(&self.value))
}
}
#[cfg(feature = "biblatex")]
impl FmtString {
pub(crate) fn extend(&mut self, f2: Self) {
self.value += &f2.value;
self.verbatim = self.verbatim || f2.verbatim;
if let Some((t1, t2)) = self
.title_case
.clone()
.and_then(|t1| Some((t1, f2.clone().title_case?)))
{
self.title_case = Some(t1 + &t2);
} else {
self.title_case = None;
}
if let Some((t1, t2)) = self
.sentence_case
.clone()
.and_then(|t1| Some((t1, f2.sentence_case?)))
{
self.sentence_case = Some(t1 + &t2);
} else {
self.sentence_case = None;
}
}
pub(crate) fn new_empty(title: bool, sentence: bool, verbatim: bool) -> Self {
Self {
value: String::new(),
title_case: if title { Some(String::new()) } else { None },
sentence_case: if sentence { Some(String::new()) } else { None },
verbatim,
}
}
}
impl AsRef<str> for FmtString {
fn as_ref(&self) -> &str {
&self.value
}
}
impl From<&str> for FmtString {
fn from(string: &str) -> FmtString {
FmtString::new(string)
}
}
impl From<String> for FmtString {
fn from(string: String) -> FmtString {
FmtString::new(string)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Title {
pub canonical: FmtString,
pub shorthand: Option<FmtString>,
pub translated: Option<FmtString>,
}
impl Title {
pub fn new(canonical: impl Into<FmtString>) -> Self {
Self {
canonical: canonical.into(),
shorthand: None,
translated: None,
}
}
pub fn shorthand(self, shorthand: impl Into<FmtString>) -> Self {
Self { shorthand: Some(shorthand.into()), ..self }
}
pub fn translated(self, translated: impl Into<FmtString>) -> Self {
Self { translated: Some(translated.into()), ..self }
}
}
pub(crate) trait FmtOptionExt<'a> {
fn value(self) -> Option<&'a str>;
}
impl<'a> FmtOptionExt<'a> for Option<&'a FmtString> {
fn value(self) -> Option<&'a str> {
self.map(|fmt| fmt.value.as_str())
}
}
impl<'a> FmtOptionExt<'a> for Option<&'a Title> {
fn value(self) -> Option<&'a str> {
self.map(|title| title.canonical.value.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QualifiedUrl {
pub value: Url,
pub visit_date: Option<Date>,
}
pub(crate) fn parse_range(source: &str) -> Option<Range<i64>> {
RANGE_REGEX.captures(source).map(|caps| {
let start: i64 = str::parse(caps.name("s").expect("start is mandatory").as_str())
.expect("Only queried for digits");
let end: i64 = caps
.name("e")
.map(|v| str::parse(v.as_str()).expect("Only queried for digits"))
.unwrap_or(start);
start..end
})
}
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub struct Duration {
pub days: u32,
pub hours: u32,
pub minutes: u32,
pub seconds: u8,
pub milliseconds: f64,
}
#[derive(Clone, Error, Debug)]
pub enum DurationError {
#[error("duration string malformed")]
Malformed,
#[error("out of bounds value when greater order value is specified")]
TooLarge,
}
impl FromStr for Duration {
type Err = DurationError;
fn from_str(source: &str) -> Result<Self, Self::Err> {
let capt = DURATION_REGEX
.captures(source.trim())
.ok_or(DurationError::Malformed)?;
let seconds: u8 = capt.name("s").unwrap().as_str().parse().unwrap();
let mut minutes: u32 = capt.name("m").unwrap().as_str().parse().unwrap();
if seconds > 59 {
return Err(DurationError::TooLarge);
}
let ms: Option<f64> = capt.name("ms").and_then(|m| m.as_str().parse().ok());
let hours: Option<u32> = capt.name("h").and_then(|m| m.as_str().parse().ok());
let days: Option<u32> = capt.name("d").and_then(|m| m.as_str().parse().ok());
if hours.is_some() && minutes > 59 {
return Err(DurationError::TooLarge);
}
let mut hours = hours.unwrap_or_else(|| {
let res = minutes / 60;
minutes %= 60;
res
});
if days.is_some() && hours > 23 {
return Err(DurationError::TooLarge);
}
let days = days.unwrap_or_else(|| {
let res = hours / 24;
hours %= 24;
res
});
Ok(Self {
days,
hours,
minutes,
seconds,
milliseconds: ms.unwrap_or(0.0),
})
}
}
impl Duration {
fn as_ms(&self) -> f64 {
self.milliseconds
+ self.seconds as f64 * 1000.0
+ self.minutes as f64 * 6000.0
+ self.hours as f64 * 36000.0
+ self.days as f64 * 864000.0
}
fn from_ms(mut ms: f64) -> Self {
let days = (ms / 864000.0) as u32;
ms -= days as f64 * 864000.0;
let hours = (ms / 36000.0) as u32;
ms -= hours as f64 * 36000.0;
let minutes = (ms / 6000.0) as u32;
ms -= minutes as f64 * 6000.0;
let seconds = (ms / 1000.0) as u8;
ms -= seconds as f64 * 1000.0;
Self { days, hours, minutes, seconds, milliseconds: ms }
}
pub fn range_from_str(source: &str) -> Result<std::ops::Range<Self>, DurationError> {
let caps = DURATION_RANGE_REGEX
.captures(source.trim())
.ok_or(DurationError::Malformed)?;
let start = Self::from_str(caps.name("s").expect("start is mandatory").as_str())?;
let end = caps
.name("e")
.map(|e| Self::from_str(e.as_str()))
.unwrap_or_else(|| Ok(start))?;
Ok(start..end)
}
}
impl PartialOrd for Duration {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let ord = self
.days
.cmp(&other.days)
.then_with(|| self.hours.cmp(&other.hours))
.then_with(|| self.minutes.cmp(&other.minutes))
.then_with(|| self.seconds.cmp(&other.seconds));
if ord == Ordering::Equal {
self.milliseconds.partial_cmp(&other.milliseconds)
} else {
Some(ord)
}
}
}
impl Sub for Duration {
type Output = Self;
fn sub(self, other: Self) -> Self {
Duration::from_ms(self.as_ms() - other.as_ms())
}
}
impl Add for Duration {
type Output = Self;
fn add(self, other: Self) -> Self {
Duration::from_ms(self.as_ms() + other.as_ms())
}
}
#[derive(Error, Debug)]
#[error("wrong type: `{0}`")]
pub struct TypeError(Value);
macro_rules! impl_try_from_value {
($variant:ident, $target:ty $(,)*) => {
impl_try_from_value!(noref $variant, $target);
impl<'s> TryFrom<&'s Value> for &'s $target {
type Error = TypeError;
fn try_from(value: &'s Value) -> Result<Self, Self::Error> {
match value {
Value::$variant(f) => Ok(f),
_ => Err(TypeError(value.clone())),
}
}
}
};
(noref $variant:ident, $target:ty $(,)*) => {
impl TryFrom<Value> for $target {
type Error = TypeError;
fn try_from(value: Value) -> Result<Self, Self::Error> {
match value {
Value::$variant(f) => Ok(f),
_ => Err(TypeError(value)),
}
}
}
impl From<$target> for Value {
fn from(value: $target) -> Self {
Value::$variant(value)
}
}
};
($variant:ident, $target:ty, $ref_target:ty $(,)*) => {
impl_try_from_value!(noref $variant, $target);
impl<'s> TryFrom<&'s Value> for &'s $ref_target {
type Error = TypeError;
fn try_from(value: &'s Value) -> Result<Self, Self::Error> {
match value {
Value::$variant(f) => Ok(f),
_ => Err(TypeError(value.clone())),
}
}
}
}
}
impl_try_from_value!(Title, Title);
impl_try_from_value!(FmtString, FmtString);
impl_try_from_value!(Text, String, str);
impl_try_from_value!(Integer, i64);
impl_try_from_value!(Date, Date);
impl_try_from_value!(Persons, Vec<Person>, [Person]);
impl_try_from_value!(
PersonsWithRoles,
Vec<(Vec<Person>, PersonRole)>,
[(Vec<Person>, PersonRole)]
);
impl_try_from_value!(IntegerOrText, NumOrStr);
impl_try_from_value!(Range, std::ops::Range<i64>);
impl_try_from_value!(Duration, Duration);
impl_try_from_value!(TimeRange, std::ops::Range<Duration>);
impl_try_from_value!(Url, QualifiedUrl);
impl_try_from_value!(Language, unic_langid::LanguageIdentifier);
impl_try_from_value!(Entries, Vec<Entry>, [Entry]);
#[cfg(test)]
mod tests {
use super::Person;
#[test]
fn person_initials() {
let p = Person::from_strings(&["Dissmer", "Courtney Deliah"]).unwrap();
assert_eq!("C. D.", p.initials(Some(".")).unwrap());
let p = Person::from_strings(&["Günther", "Hans-Joseph"]).unwrap();
assert_eq!("H-J", p.initials(None).unwrap());
}
}