use crate::Node;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct PersonalName {
pub original: String,
pub given: Option<String>,
pub surname: Option<String>,
pub surname_prefix: Option<String>,
pub prefix: Option<String>,
pub suffix: Option<String>,
pub nickname: Option<String>,
pub kind: Option<String>,
}
impl PersonalName {
#[must_use]
pub fn from_node(node: &Node) -> Self {
let original = node.logical_value();
let mut name = Self::from_payload(&original);
for (tag, field) in [
("GIVN", &mut name.given),
("SURN", &mut name.surname),
("SPFX", &mut name.surname_prefix),
("NPFX", &mut name.prefix),
("NSFX", &mut name.suffix),
("NICK", &mut name.nickname),
("TYPE", &mut name.kind),
] {
if let Some(value) = node.value_of(tag).filter(|value| !value.trim().is_empty()) {
*field = Some(value.trim().to_owned());
}
}
name
}
#[must_use]
pub fn from_payload(payload: &str) -> Self {
let original = payload.to_owned();
let mut name = Self {
original,
..Self::default()
};
let opening = payload.find('/');
let closing =
opening.and_then(|start| payload[start + 1..].find('/').map(|end| start + 1 + end));
match (opening, closing) {
(Some(start), Some(end)) => {
name.given = trimmed(&payload[..start]);
name.surname = trimmed(&payload[start + 1..end]);
name.suffix = trimmed(&payload[end + 1..]);
}
(Some(start), None) => {
name.given = trimmed(&payload[..start]);
name.surname = trimmed(&payload[start + 1..]);
}
_ => name.given = trimmed(payload),
}
name
}
#[must_use]
pub fn display(&self) -> String {
let mut parts = Vec::new();
if let Some(prefix) = &self.prefix {
parts.push(prefix.as_str());
}
if let Some(given) = &self.given {
parts.push(given.as_str());
}
if let Some(prefix) = &self.surname_prefix {
parts.push(prefix.as_str());
}
if let Some(surname) = &self.surname {
parts.push(surname.as_str());
}
if let Some(suffix) = &self.suffix {
parts.push(suffix.as_str());
}
if parts.is_empty() {
return self.original.replace('/', "").trim().to_owned();
}
parts.join(" ")
}
#[must_use]
pub fn sort_form(&self) -> String {
match (&self.surname, &self.given) {
(Some(surname), Some(given)) => format!("{surname}, {given}"),
(Some(surname), None) => surname.clone(),
(None, Some(given)) => given.clone(),
(None, None) => self.display(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.given.is_none() && self.surname.is_none() && self.original.trim().is_empty()
}
}
fn trimmed(value: &str) -> Option<String> {
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.to_owned())
}
#[must_use]
pub fn slashed(value: &str) -> String {
let trimmed = value.trim();
if trimmed.contains('/') {
return trimmed.to_owned();
}
match trimmed.rsplit_once(' ') {
Some((given, surname)) => format!("{given} /{surname}/"),
None => format!("/{trimmed}/"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_slashed_form_splits_into_pieces() {
let name = PersonalName::from_payload("Mira Elowen /North Quill/");
assert_eq!(name.given.as_deref(), Some("Mira Elowen"));
assert_eq!(name.surname.as_deref(), Some("North Quill"));
assert_eq!(name.display(), "Mira Elowen North Quill");
assert_eq!(name.sort_form(), "North Quill, Mira Elowen");
}
#[test]
fn a_suffix_after_the_surname_is_kept() {
let name = PersonalName::from_payload("Rowan /Suffix-Test/ Jr.");
assert_eq!(name.suffix.as_deref(), Some("Jr."));
assert_eq!(name.display(), "Rowan Suffix-Test Jr.");
}
#[test]
fn a_name_with_no_slashes_is_not_guessed_at() {
let name = PersonalName::from_payload("Unslashed Example");
assert_eq!(name.given.as_deref(), Some("Unslashed Example"));
assert_eq!(name.surname, None);
assert_eq!(name.display(), "Unslashed Example");
}
#[test]
fn an_unclosed_slash_still_yields_a_surname() {
let name = PersonalName::from_payload("Rill /Unclosed-Test");
assert_eq!(name.surname.as_deref(), Some("Unclosed-Test"));
}
#[test]
fn substructures_override_only_the_piece_they_name() {
let node = Node::with_value("NAME", "Mira Elowen /North Quill/")
.child(Node::with_value("SURN", "Quill"))
.child(Node::with_value("NICK", "Miri"))
.child(Node::with_value("TYPE", "birth"));
let name = PersonalName::from_node(&node);
assert_eq!(name.given.as_deref(), Some("Mira Elowen"));
assert_eq!(name.surname.as_deref(), Some("Quill"));
assert_eq!(name.nickname.as_deref(), Some("Miri"));
assert_eq!(name.kind.as_deref(), Some("birth"));
}
#[test]
fn a_surname_particle_sorts_with_the_surname_and_shows_before_it() {
let node = Node::with_value("NAME", "Wren /River-Test/")
.child(Node::with_value("SPFX", "van"))
.child(Node::with_value("NPFX", "Dr"));
let name = PersonalName::from_node(&node);
assert_eq!(name.display(), "Dr Wren van River-Test");
}
#[test]
fn an_empty_name_is_recognized_rather_than_shown_as_blank() {
assert!(PersonalName::from_payload("").is_empty());
assert!(!PersonalName::from_payload("//").is_empty());
}
#[test]
fn a_typed_name_gains_slashes_and_a_slashed_one_is_left_alone() {
assert_eq!(slashed("Ada Example"), "Ada /Example/");
assert_eq!(slashed("Madonna"), "/Madonna/");
assert_eq!(slashed(" Ada /van Example/ "), "Ada /van Example/");
}
}