use std::borrow::Cow;
use std::cmp::Ordering;
use std::str::FromStr;
use citationberg::LongShortForm;
use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use unicode_segmentation::UnicodeSegmentation;
use crate::lang::en::ARTICLES;
use crate::lang::is_cjk;
use crate::lang::name::NAME_PARTICLES;
use crate::util::{deserialize_one_or_many, serialize_one_or_many};
use super::derive_or_from_str;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct PersonsWithRoles {
#[serde(serialize_with = "serialize_one_or_many")]
#[serde(deserialize_with = "deserialize_one_or_many")]
pub names: Vec<Person>,
pub role: PersonRole,
}
impl PersonsWithRoles {
pub fn new(names: Vec<Person>, role: PersonRole) -> Self {
Self { names, role }
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[serde(rename_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,
#[serde(skip)]
Unknown(String),
}
derive_or_from_str! {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Person where "a name string or a dictionary with a \"name\" key" {
pub name: String,
pub given_name: Option<String>,
pub prefix: Option<String>,
pub suffix: Option<String>,
pub alias: Option<String>,
}
}
impl Serialize for Person {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if self.alias.is_none() && self.prefix.is_none() {
serializer.serialize_str(&self.name_first(false, false))
} else {
let entries = [
("name", Some(&self.name)),
("given-name", self.given_name.as_ref()),
("prefix", self.prefix.as_ref()),
("suffix", self.suffix.as_ref()),
("alias", self.alias.as_ref()),
];
let map_len = entries.iter().filter(|(_, v)| v.is_some()).count();
let mut map = serializer.serialize_map(Some(map_len))?;
for (key, value) in entries.iter() {
if let Some(value) = value {
map.serialize_entry(key, value)?;
}
}
map.end()
}
}
}
#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
pub enum PersonError {
#[error("too many parts")]
TooManyParts,
#[error("part list is empty")]
Empty,
#[error("unknown role")]
UnknownRole,
}
impl Person {
pub fn from_strings(mut parts: Vec<&str>) -> Result<Self, PersonError> {
if parts.is_empty() {
return Err(PersonError::Empty);
} else if parts.len() > 3 {
return Err(PersonError::TooManyParts);
}
for part in parts.iter_mut() {
*part = part.trim();
}
let last_pre = parts[0];
let given_name = (parts.len() > 1)
.then(|| parts.last().map(|last| last.to_string()))
.flatten();
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,
buf: &mut impl std::fmt::Write,
delimiter: Option<&str>,
with_hyphen: bool,
) -> std::fmt::Result {
let Some(gn) = &self.given_name else {
return Ok(());
};
let mut collect = true;
let mut non_empty = false;
for (_, gr) in gn.grapheme_indices(true) {
if let Some(c) = gr.chars().next()
&& (c.is_whitespace() || c == '-')
{
if !collect {
let hyphenate = with_hyphen && c == '-';
if let Some(delimiter) = delimiter {
buf.write_str(if hyphenate {
delimiter.trim_end()
} else {
delimiter
})?;
}
collect = true;
if hyphenate {
buf.write_char('-')?;
}
}
continue;
}
if collect {
buf.write_str(gr)?;
collect = false;
non_empty = true;
}
}
if non_empty
&& !collect
&& let Some(delim) = delimiter
{
buf.write_str(delim.trim_end())?;
}
Ok(())
}
pub fn first_name_with_delimiter(
&self,
buf: &mut impl std::fmt::Write,
delimiter: Option<&str>,
) -> std::fmt::Result {
let Some(name) = &self.given_name else {
return Ok(());
};
let mut first = true;
for item in name.split(' ') {
if !first {
buf.write_char(' ')?;
}
buf.write_str(item)?;
if let Some(delimiter) = delimiter
&& item.graphemes(true).count() == 1
{
buf.write_str(delimiter)?;
}
first = false;
}
Ok(())
}
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 self.given_name.is_some() {
res += ", ";
self.initials(&mut res, Some(". "), true).unwrap();
}
} else if let Some(given_name) = &self.given_name {
res += ", ";
res += given_name;
}
if prefix_given_name && 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 = String::new();
if initials {
if self.given_name.is_some() {
self.initials(&mut res, Some(". "), true).unwrap();
res.push(' ');
}
} else if let Some(given_name) = &self.given_name {
res += given_name;
res.push(' ');
}
if let Some(prefix) = &self.prefix {
res += prefix;
res += " ";
}
res += &self.name;
if let Some(suffix) = &self.suffix {
res += " ";
res += suffix;
}
res
}
pub fn name_particle(&self) -> Option<&str> {
for (idx, char) in self.name.char_indices().rev() {
if char != ' ' {
continue;
}
let particle = &self.name[0..idx];
let lowercase = particle.to_lowercase();
if NAME_PARTICLES.binary_search(&lowercase.as_str()).is_ok() {
return Some(particle);
}
}
None
}
pub fn name_without_particle(&self) -> &str {
if let Some(particle) = self.name_particle() {
self.name[particle.len()..].trim_start()
} else {
self.name.as_str()
}
}
pub fn name_particles(&self) -> Option<Cow<'_, str>> {
match (&self.prefix, self.name_particle()) {
(Some(dropping), Some(non_dropping)) => {
Some(Cow::Owned(format!("{dropping} {non_dropping}")))
}
(Some(dropping), None) => Some(Cow::Borrowed(dropping.as_str())),
(None, Some(non_dropping)) => Some(Cow::Borrowed(non_dropping)),
(None, None) => None,
}
}
pub fn is_institutional(&self) -> bool {
self.given_name.is_none() && self.suffix.is_none() && self.prefix.is_none()
}
pub fn is_cjk(&self) -> bool {
self.name.chars().any(is_cjk)
|| self.given_name.as_ref().is_some_and(|gn| gn.chars().any(is_cjk))
}
pub fn name_without_article(&self) -> &str {
let space_idx = self.name.find(' ');
if let Some(space_idx) = space_idx {
if space_idx + 1 != self.name.len()
&& ARTICLES.binary_search(&&self.name[0..space_idx]).is_ok()
{
&self.name[space_idx + 1..]
} else {
self.name.as_str()
}
} else {
self.name.as_str()
}
}
pub(crate) fn csl_cmp(
&self,
other: &Self,
form: LongShortForm,
demote_particle: bool,
) -> std::cmp::Ordering {
let self_cjk = self.is_cjk();
let other_cjk = other.is_cjk();
let name_a = self.name.to_lowercase();
let name_b = other.name.to_lowercase();
let given_a = self.given_name.as_ref().map(|s| s.to_lowercase());
let given_b = other.given_name.as_ref().map(|s| s.to_lowercase());
if self_cjk != other_cjk {
return self_cjk.cmp(&other_cjk);
} else if self_cjk && other_cjk {
return match form {
LongShortForm::Long => {
name_a.cmp(&name_b).then_with(|| given_a.cmp(&given_b))
}
LongShortForm::Short => name_a.cmp(&name_b),
};
}
let prefix_a = self.prefix.as_ref().map(|s| s.to_lowercase());
let prefix_b = other.prefix.as_ref().map(|s| s.to_lowercase());
let suffix_a = self.suffix.as_ref().map(|s| s.to_lowercase());
let suffix_b = other.suffix.as_ref().map(|s| s.to_lowercase());
if demote_particle {
self.name_without_particle()
.to_lowercase()
.cmp(&other.name_without_particle().to_lowercase())
.then_with(|| {
self.name_particles()
.map(|c| c.to_lowercase())
.cmp(&other.name_particles().map(|c| c.to_lowercase()))
})
.then_with(|| given_a.cmp(&given_b))
.then_with(|| suffix_a.cmp(&suffix_b))
} else {
name_a
.cmp(&name_b)
.then_with(|| prefix_a.cmp(&prefix_b))
.then_with(|| given_a.cmp(&given_b))
.then_with(|| suffix_a.cmp(&suffix_b))
}
}
}
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))
}
}
impl FromStr for Person {
type Err = PersonError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_strings(s.split(',').collect())
}
}
#[cfg(test)]
mod tests {
use super::Person;
#[test]
fn person_initials() {
let mut s = String::new();
let p = Person::from_strings(vec!["Dissmer", "Courtney Deliah"]).unwrap();
p.initials(&mut s, Some("."), true).unwrap();
assert_eq!("C.D.", s);
let mut s = String::new();
let p = Person::from_strings(vec!["Dissmer", "Courtney Deliah"]).unwrap();
p.initials(&mut s, Some(". "), true).unwrap();
assert_eq!("C. D.", s);
let mut s = String::new();
let p = Person::from_strings(vec!["Dissmer", "Courtney Deliah"]).unwrap();
p.initials(&mut s, Some(""), true).unwrap();
assert_eq!("CD", s);
let mut s = String::new();
let p = Person::from_strings(vec!["Dissmer", "Courtney Deliah"]).unwrap();
p.initials(&mut s, None, true).unwrap();
assert_eq!("CD", s);
let mut s = String::new();
let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
p.initials(&mut s, Some(". "), true).unwrap();
assert_eq!("H.-J.", s);
let mut s = String::new();
let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
p.initials(&mut s, Some("."), true).unwrap();
assert_eq!("H.-J.", s);
let mut s = String::new();
let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
p.initials(&mut s, Some(""), true).unwrap();
assert_eq!("H-J", s);
let mut s = String::new();
let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
p.initials(&mut s, None, true).unwrap();
assert_eq!("H-J", s);
let mut s = String::new();
let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
p.initials(&mut s, Some(". "), false).unwrap();
assert_eq!("H. J.", s);
let mut s = String::new();
let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
p.initials(&mut s, Some("."), false).unwrap();
assert_eq!("H.J.", s);
let mut s = String::new();
let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
p.initials(&mut s, Some(""), false).unwrap();
assert_eq!("HJ", s);
let mut s = String::new();
let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
p.initials(&mut s, None, false).unwrap();
assert_eq!("HJ", s);
}
#[test]
fn person_non_dropping_initials() {
let p = Person::from_strings(vec!["Von Der Leyen", "Ursula"]).unwrap();
assert_eq!("Von Der", p.name_particle().unwrap());
assert_eq!("Leyen", p.name_without_particle());
}
#[test]
fn person_middle_initial() {
let p = Person::from_strings(vec!["Kirk", "James T"]).unwrap();
let mut s = String::new();
p.first_name_with_delimiter(&mut s, Some(".")).unwrap();
assert_eq!("James T.", s);
}
#[test]
fn person_name_retrieval_order() {
let p =
Person::from_strings(vec!["van Dissmer", "Jr.", "Courtney Deliah"]).unwrap();
assert_eq!("van Dissmer, Courtney Deliah, Jr.", p.name_first(false, false));
assert_eq!("Dissmer, Courtney Deliah van, Jr.", p.name_first(false, true));
assert_eq!("van Dissmer, C. D., Jr.", p.name_first(true, false));
assert_eq!("Dissmer, C. D. van, Jr.", p.name_first(true, true));
assert_eq!("Courtney Deliah van Dissmer Jr.", p.given_first(false));
assert_eq!("C. D. van Dissmer Jr.", p.given_first(true));
}
}