use crate::Document;
use std::collections::HashMap;
const MAX_GENERATIONS: u32 = 128;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct FamilyMembers {
pub partners: Vec<String>,
pub children: Vec<String>,
}
#[derive(Clone, Debug, Default)]
pub struct FamilyGraph {
parents: HashMap<String, Vec<String>>,
children: HashMap<String, Vec<String>>,
partners: HashMap<String, Vec<String>>,
families: HashMap<String, FamilyMembers>,
}
impl FamilyGraph {
#[must_use]
pub fn new(document: &Document) -> Self {
let mut graph = Self::default();
for record in document.records_of("FAM") {
let Some(xref) = &record.xref else { continue };
let members = graph.families.entry(key(xref)).or_default();
for tag in ["HUSB", "WIFE"] {
for node in record.all(tag) {
if let Some(pointer) = node.pointer().filter(|value| *value != "@VOID@") {
push_unique(&mut members.partners, pointer);
}
}
}
for node in record.all("CHIL") {
if let Some(pointer) = node.pointer().filter(|value| *value != "@VOID@") {
push_unique(&mut members.children, pointer);
}
}
}
for record in document.records_of("INDI") {
let Some(xref) = &record.xref else { continue };
for (tag, side) in [("FAMC", Side::Child), ("FAMS", Side::Partner)] {
for link in record.all(tag) {
let Some(family) = link.pointer().filter(|value| *value != "@VOID@") else {
continue;
};
let members = graph.families.entry(key(family)).or_default();
match side {
Side::Child => push_unique(&mut members.children, xref),
Side::Partner => push_unique(&mut members.partners, xref),
}
}
}
}
for members in graph.families.values() {
for (index, one) in members.partners.iter().enumerate() {
for other in &members.partners[index + 1..] {
push_unique(graph.partners.entry(key(one)).or_default(), other);
push_unique(graph.partners.entry(key(other)).or_default(), one);
}
}
for child in &members.children {
for parent in &members.partners {
push_unique(graph.parents.entry(key(child)).or_default(), parent);
push_unique(graph.children.entry(key(parent)).or_default(), child);
}
}
}
graph
}
#[must_use]
pub fn parents_of(&self, xref: &str) -> &[String] {
lookup(&self.parents, xref)
}
#[must_use]
pub fn children_of(&self, xref: &str) -> &[String] {
lookup(&self.children, xref)
}
#[must_use]
pub fn partners_of(&self, xref: &str) -> &[String] {
lookup(&self.partners, xref)
}
#[must_use]
pub fn family(&self, xref: &str) -> Option<&FamilyMembers> {
self.families.get(&key(xref))
}
#[must_use]
pub const fn families(&self) -> &HashMap<String, FamilyMembers> {
&self.families
}
#[must_use]
pub fn ancestors(&self, xref: &str) -> HashMap<String, u32> {
Self::walk(xref, &self.parents)
}
#[must_use]
pub fn descendants(&self, xref: &str) -> HashMap<String, u32> {
Self::walk(xref, &self.children)
}
fn walk(start: &str, along: &HashMap<String, Vec<String>>) -> HashMap<String, u32> {
let mut found: HashMap<String, u32> = HashMap::new();
let mut frontier = vec![key(start)];
found.insert(key(start), 0);
for generation in 1..=MAX_GENERATIONS {
let mut next = Vec::new();
for person in frontier {
for linked in along.get(&person).into_iter().flatten() {
let linked = key(linked);
if !found.contains_key(&linked) {
found.insert(linked.clone(), generation);
next.push(linked);
}
}
}
if next.is_empty() {
break;
}
frontier = next;
}
found
}
}
enum Side {
Child,
Partner,
}
fn key(xref: &str) -> String {
xref.to_ascii_uppercase()
}
fn lookup<'a>(map: &'a HashMap<String, Vec<String>>, xref: &str) -> &'a [String] {
map.get(&key(xref)).map_or(&[], Vec::as_slice)
}
fn push_unique(list: &mut Vec<String>, value: &str) {
if !list.iter().any(|held| held.eq_ignore_ascii_case(value)) {
list.push(value.to_owned());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn graph(text: &str) -> FamilyGraph {
FamilyGraph::new(&Document::parse(text).expect("parse"))
}
#[test]
fn a_family_written_from_both_sides_is_read_once() {
let graph = graph(
"0 HEAD\n\
0 @I1@ INDI\n1 FAMS @F1@\n\
0 @I2@ INDI\n1 FAMS @F1@\n\
0 @I3@ INDI\n1 FAMC @F1@\n\
0 @F1@ FAM\n1 HUSB @I1@\n1 WIFE @I2@\n1 CHIL @I3@\n\
0 TRLR\n",
);
assert_eq!(graph.parents_of("@I3@"), ["@I1@", "@I2@"]);
assert_eq!(graph.children_of("@I1@"), ["@I3@"]);
assert_eq!(graph.partners_of("@I2@"), ["@I1@"]);
let family = graph.family("@F1@").expect("family");
assert_eq!(family.partners, ["@I1@", "@I2@"]);
assert_eq!(family.children, ["@I3@"]);
}
#[test]
fn a_file_that_writes_only_famc_still_draws_the_whole_family() {
let graph = graph(
"0 HEAD\n\
0 @I1@ INDI\n1 FAMS @F1@\n\
0 @I3@ INDI\n1 FAMC @F1@\n\
0 @I4@ INDI\n1 FAMC @F1@\n\
0 TRLR\n",
);
assert_eq!(graph.parents_of("@I3@"), ["@I1@"]);
assert_eq!(graph.children_of("@I1@"), ["@I3@", "@I4@"]);
assert_eq!(
graph.family("@F1@").expect("linked family").children,
["@I3@", "@I4@"]
);
}
#[test]
fn the_two_directions_disagreeing_reads_as_the_union() {
let graph = graph(
"0 HEAD\n\
0 @I1@ INDI\n\
0 @I2@ INDI\n1 FAMC @F1@\n\
0 @F1@ FAM\n1 HUSB @I1@\n1 CHIL @I3@\n\
0 @I3@ INDI\n\
0 TRLR\n",
);
let family = graph.family("@F1@").expect("family");
assert_eq!(family.children, ["@I3@", "@I2@"]);
assert_eq!(graph.children_of("@I1@"), ["@I3@", "@I2@"]);
}
#[test]
fn identifiers_match_case_insensitively_and_void_is_no_relative() {
let graph = graph(
"0 HEAD\n\
0 @i1@ INDI\n1 FAMS @f1@\n\
0 @I2@ INDI\n1 FAMC @F1@\n1 FAMC @VOID@\n\
0 TRLR\n",
);
assert_eq!(graph.parents_of("@I2@"), ["@i1@"]);
assert_eq!(graph.children_of("@I1@"), ["@I2@"]);
assert!(graph.family("@VOID@").is_none());
}
#[test]
fn a_cyclic_file_terminates_with_everyone_counted_once() {
let graph = graph(
"0 HEAD\n\
0 @I1@ INDI\n1 FAMC @F1@\n1 FAMS @F2@\n\
0 @I2@ INDI\n1 FAMS @F1@\n1 FAMC @F2@\n\
0 TRLR\n",
);
let up = graph.ancestors("@I1@");
assert_eq!(up.get("@I1@"), Some(&0));
assert_eq!(up.get("@I2@"), Some(&1));
assert_eq!(up.len(), 2);
}
#[test]
fn ancestors_and_descendants_count_generations() {
let graph = graph(
"0 HEAD\n\
0 @I1@ INDI\n\
0 @I2@ INDI\n1 FAMC @F1@\n\
0 @I3@ INDI\n1 FAMC @F2@\n\
0 @F1@ FAM\n1 HUSB @I1@\n\
0 @F2@ FAM\n1 HUSB @I2@\n\
0 TRLR\n",
);
let down = graph.descendants("@I1@");
assert_eq!(down.get("@I2@"), Some(&1));
assert_eq!(down.get("@I3@"), Some(&2));
let up = graph.ancestors("@I3@");
assert_eq!(up.get("@I1@"), Some(&2));
}
}