From 7936fa122111e1d2161ab66c7f4fa0d31a16bb16 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sat, 6 Dec 2025 19:02:27 -0500 Subject: [PATCH 1/8] Add comprehensive relationship and search API with benchmarks - Add search functions: find_individual_by_xref, find_individuals_by_name, find_family_by_xref, find_individuals_by_event_date - Add basic relationship functions: get_parents, get_children, get_spouses, get_siblings, get_full_siblings, get_half_siblings - Add advanced relationship functions: get_ancestors, get_descendants, find_relationship_path, find_relationship - Add RelationshipResult type with MRCA tracking and human-readable descriptions - Add EventType enum supporting 20+ life events (Birth, Death, Christening, etc.) - Implement relationship detection: Parent/Child, Sibling/Half-Sibling, Cousins (1st, 2nd, etc.), Removed cousins (1x, 2x, etc.) - Fix sibling detection to properly identify half-siblings across different family records - Add comprehensive benchmarks for all new functions - Add 5 example programs demonstrating the API - All 271 tests passing --- benches/parse_gedcom.rs | 111 ++- examples/advanced_relationships.rs | 259 ++++++ examples/basic_relationships.rs | 238 +++++ examples/find_relationship.rs | 242 +++++ examples/search_by_date.rs | 165 ++++ examples/search_individuals.rs | 169 ++++ src/types/mod.rs | 1393 ++++++++++++++++++++++++++++ 7 files changed, 2574 insertions(+), 3 deletions(-) create mode 100644 examples/advanced_relationships.rs create mode 100644 examples/basic_relationships.rs create mode 100644 examples/find_relationship.rs create mode 100644 examples/search_by_date.rs create mode 100644 examples/search_individuals.rs diff --git a/benches/parse_gedcom.rs b/benches/parse_gedcom.rs index fc7af3e..f128bb1 100644 --- a/benches/parse_gedcom.rs +++ b/benches/parse_gedcom.rs @@ -1,11 +1,12 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; use gedcom_rs::parse::{parse_gedcom, GedcomConfig}; +use gedcom_rs::types::EventType; use std::time::Duration; const FILENAME: &str = "data/complete.ged"; -fn criterion_benchmark(c: &mut Criterion) { +fn parse_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("parse-gedcom"); group.measurement_time(Duration::from_secs(30)); @@ -19,5 +20,109 @@ fn criterion_benchmark(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, criterion_benchmark); +fn search_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("search"); + group.measurement_time(Duration::from_secs(5)); + + let config = GedcomConfig::new(); + let gedcom = parse_gedcom(FILENAME, &config).expect("Failed to parse GEDCOM"); + + group.bench_function("find_individual_by_xref", |b| { + b.iter(|| gedcom.find_individual_by_xref(black_box("@I1@"))) + }); + + group.bench_function("find_individuals_by_name", |b| { + b.iter(|| gedcom.find_individuals_by_name(black_box("Torture"))) + }); + + group.bench_function("find_family_by_xref", |b| { + b.iter(|| gedcom.find_family_by_xref(black_box("@F1@"))) + }); + + group.bench_function("find_individuals_by_event_date", |b| { + b.iter(|| { + gedcom.find_individuals_by_event_date(black_box(EventType::Birth), black_box("1965")) + }) + }); + + group.finish(); +} + +fn basic_relationships_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("basic-relationships"); + group.measurement_time(Duration::from_secs(5)); + + let config = GedcomConfig::new(); + let gedcom = parse_gedcom(FILENAME, &config).expect("Failed to parse GEDCOM"); + let person = gedcom + .find_individual_by_xref("@I1@") + .expect("Failed to find person"); + + group.bench_function("get_parents", |b| { + b.iter(|| gedcom.get_parents(black_box(person))) + }); + + group.bench_function("get_children", |b| { + b.iter(|| gedcom.get_children(black_box(person))) + }); + + group.bench_function("get_spouses", |b| { + b.iter(|| gedcom.get_spouses(black_box(person))) + }); + + group.bench_function("get_siblings", |b| { + b.iter(|| gedcom.get_siblings(black_box(person))) + }); + + group.bench_function("get_full_siblings", |b| { + b.iter(|| gedcom.get_full_siblings(black_box(person))) + }); + + group.bench_function("get_half_siblings", |b| { + b.iter(|| gedcom.get_half_siblings(black_box(person))) + }); + + group.finish(); +} + +fn advanced_relationships_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("advanced-relationships"); + group.measurement_time(Duration::from_secs(5)); + + let config = GedcomConfig::new(); + let gedcom = parse_gedcom(FILENAME, &config).expect("Failed to parse GEDCOM"); + let person = gedcom + .find_individual_by_xref("@I1@") + .expect("Failed to find person"); + + group.bench_function("get_ancestors", |b| { + b.iter(|| gedcom.get_ancestors(black_box(person), black_box(Some(5)))) + }); + + group.bench_function("get_descendants", |b| { + b.iter(|| gedcom.get_descendants(black_box(person), black_box(Some(5)))) + }); + + let person2 = gedcom + .find_individual_by_xref("@I3@") + .expect("Failed to find person 2"); + + group.bench_function("find_relationship_path", |b| { + b.iter(|| gedcom.find_relationship_path(black_box(person), black_box(person2))) + }); + + group.bench_function("find_relationship", |b| { + b.iter(|| gedcom.find_relationship(black_box(person), black_box(person2))) + }); + + group.finish(); +} + +criterion_group!( + benches, + parse_benchmark, + search_benchmark, + basic_relationships_benchmark, + advanced_relationships_benchmark +); criterion_main!(benches); diff --git a/examples/advanced_relationships.rs b/examples/advanced_relationships.rs new file mode 100644 index 0000000..267f7b8 --- /dev/null +++ b/examples/advanced_relationships.rs @@ -0,0 +1,259 @@ +//! Explore ancestral and descendant relationships in a GEDCOM file +//! +//! This example demonstrates the advanced relationship functions: +//! - get_ancestors: Find all ancestors up to N generations +//! - get_descendants: Find all descendants up to N generations +//! - find_relationship_path: Find the connection between two individuals +//! +//! Usage: +//! cargo run --example advanced_relationships path/to/file.ged [xref] [max_generations] +//! +//! If no xref is provided, it will use the first individual in the file. +//! If no max_generations is provided, it will find all ancestors/descendants. + +use gedcom_rs::parse::{parse_gedcom, GedcomConfig}; +use std::env; +use std::process; + +fn get_name(individual: &gedcom_rs::types::Individual) -> String { + individual + .names + .first() + .and_then(|n| n.name.value.as_ref()) + .map(|s| s.to_string()) + .unwrap_or_else(|| "(unnamed)".to_string()) +} + +fn get_xref_str(individual: &gedcom_rs::types::Individual) -> String { + individual + .xref + .as_ref() + .map(|x| x.to_string()) + .unwrap_or_else(|| "(no ID)".to_string()) +} + +fn get_birth_year(individual: &gedcom_rs::types::Individual) -> Option { + individual + .birth + .first() + .and_then(|b| b.event.detail.date.as_ref()) + .map(|d| { + // Try to extract year from date string + d.split_whitespace().last().unwrap_or(d).to_string() + }) +} + +fn main() { + // Get command line arguments + let args: Vec = env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: {} [xref] [max_generations]", args[0]); + eprintln!("\nExplores ancestors and descendants of an individual"); + eprintln!("\nExamples:"); + eprintln!(" cargo run --example advanced_relationships data/TGC551.ged"); + eprintln!(" cargo run --example advanced_relationships data/TGC551.ged @I1@"); + eprintln!(" cargo run --example advanced_relationships data/TGC551.ged @I1@ 5"); + process::exit(1); + } + + let filename = &args[1]; + let max_generations = if args.len() >= 4 { + match args[3].parse::() { + Ok(n) => Some(n), + Err(_) => { + eprintln!("Error: max_generations must be a positive number"); + process::exit(1); + } + } + } else { + None + }; + + // Parse the GEDCOM file + println!("Parsing GEDCOM file: {}", filename); + let gedcom = match parse_gedcom(filename, &GedcomConfig::new()) { + Ok(g) => g, + Err(e) => { + eprintln!("Error parsing GEDCOM file: {}", e); + process::exit(1); + } + }; + + println!( + "Successfully parsed {} individuals and {} families\n", + gedcom.individuals.len(), + gedcom.families.len() + ); + + // Determine which individual to examine + let person = if args.len() >= 3 { + let xref = &args[2]; + match gedcom.find_individual_by_xref(xref) { + Some(p) => p, + None => { + eprintln!("Error: Individual with ID {} not found", xref); + process::exit(1); + } + } + } else { + // Use first individual with family connections + gedcom + .individuals + .iter() + .find(|i| !i.famc.is_empty() || !i.fams.is_empty()) + .or_else(|| gedcom.individuals.first()) + .expect("No individuals found in GEDCOM file") + }; + + println!("=== Examining Relationships for ==="); + println!("Name: {}", get_name(person)); + println!("ID: {}", get_xref_str(person)); + if let Some(year) = get_birth_year(person) { + println!("Birth: {}", year); + } + if let Some(max_gen) = max_generations { + println!("Max generations: {}", max_gen); + } else { + println!("Max generations: unlimited"); + } + println!(); + + // Get ancestors + println!("=== Ancestors ==="); + let ancestors = gedcom.get_ancestors(person, max_generations); + if ancestors.is_empty() { + println!("No ancestors found"); + } else { + println!("Found {} ancestor(s):", ancestors.len()); + for (i, ancestor) in ancestors.iter().enumerate() { + print!( + "{}. {} ({})", + i + 1, + get_name(ancestor), + get_xref_str(ancestor) + ); + if let Some(year) = get_birth_year(ancestor) { + print!(" - b. {}", year); + } + println!(); + } + } + println!(); + + // Get descendants + println!("=== Descendants ==="); + let descendants = gedcom.get_descendants(person, max_generations); + if descendants.is_empty() { + println!("No descendants found"); + } else { + println!("Found {} descendant(s):", descendants.len()); + for (i, descendant) in descendants.iter().enumerate() { + print!( + "{}. {} ({})", + i + 1, + get_name(descendant), + get_xref_str(descendant) + ); + if let Some(year) = get_birth_year(descendant) { + print!(" - b. {}", year); + } + println!(); + } + } + println!(); + + // Demonstrate relationship path finding + println!("=== Relationship Path ==="); + if ancestors.is_empty() && descendants.is_empty() { + println!("No relationships to explore"); + } else { + // Try to find path between this person and an ancestor or descendant + let target = ancestors + .first() + .copied() + .or_else(|| descendants.first().copied()) + .or_else(|| { + // Try to find any other individual to demonstrate the path finding + gedcom + .individuals + .iter() + .find(|i| i.xref.as_ref() != person.xref.as_ref()) + }); + + if let Some(target_person) = target { + println!( + "Finding path from {} to {}", + get_name(person), + get_name(target_person) + ); + + match gedcom.find_relationship_path(person, target_person) { + Some(path) => { + println!("Found path with {} step(s):", path.len()); + for (i, step) in path.iter().enumerate() { + let arrow = if i < path.len() - 1 { " → " } else { "" }; + print!("{}{}", get_name(step), arrow); + } + println!(); + } + None => { + println!("No relationship path found between these individuals"); + } + } + } + } + println!(); + + // Summary statistics + println!("=== Summary ==="); + println!("Total ancestors found: {}", ancestors.len()); + println!("Total descendants found: {}", descendants.len()); + + if max_generations.is_none() { + println!("\nNote: These counts represent ALL known ancestors and descendants in the GEDCOM file."); + } else { + println!( + "\nNote: These counts are limited to {} generation(s).", + max_generations.unwrap() + ); + } + + // Additional statistics + if !ancestors.is_empty() { + let oldest_ancestor = ancestors + .iter() + .filter_map(|a| { + get_birth_year(a) + .and_then(|y| y.parse::().ok()) + .map(|year| (a, year)) + }) + .min_by_key(|(_, year)| *year); + + if let Some((ancestor, year)) = oldest_ancestor { + println!( + "\nOldest known ancestor: {} (b. {})", + get_name(ancestor), + year + ); + } + } + + if !descendants.is_empty() { + let youngest_descendant = descendants + .iter() + .filter_map(|d| { + get_birth_year(d) + .and_then(|y| y.parse::().ok()) + .map(|year| (d, year)) + }) + .max_by_key(|(_, year)| *year); + + if let Some((descendant, year)) = youngest_descendant { + println!( + "Youngest known descendant: {} (b. {})", + get_name(descendant), + year + ); + } + } +} diff --git a/examples/basic_relationships.rs b/examples/basic_relationships.rs new file mode 100644 index 0000000..9f65408 --- /dev/null +++ b/examples/basic_relationships.rs @@ -0,0 +1,238 @@ +//! Explore basic family relationships in a GEDCOM file +//! +//! This example demonstrates the basic relationship functions: +//! - get_parents: Find the parents of an individual +//! - get_children: Find all children of an individual +//! - get_spouses: Find all spouses of an individual +//! - get_siblings: Find all siblings of an individual +//! - get_full_siblings: Find full siblings (same mother and father) +//! - get_half_siblings: Find half-siblings (one shared parent) +//! +//! Usage: +//! cargo run --example basic_relationships path/to/file.ged [xref] +//! +//! If no xref is provided, it will use the first individual in the file. + +use gedcom_rs::parse::{parse_gedcom, GedcomConfig}; +use std::env; +use std::process; + +fn get_name(individual: &gedcom_rs::types::Individual) -> String { + individual + .names + .first() + .and_then(|n| n.name.value.as_ref()) + .map(|s| s.to_string()) + .unwrap_or_else(|| "(unnamed)".to_string()) +} + +fn get_xref_str(individual: &gedcom_rs::types::Individual) -> String { + individual + .xref + .as_ref() + .map(|x| x.to_string()) + .unwrap_or_else(|| "(no ID)".to_string()) +} + +fn main() { + // Get filename from command line arguments + let args: Vec = env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: {} [xref]", args[0]); + eprintln!("\nExplores family relationships for an individual"); + eprintln!("\nExample:"); + eprintln!(" cargo run --example basic_relationships data/TGC551.ged"); + eprintln!(" cargo run --example basic_relationships data/TGC551.ged @I1@"); + process::exit(1); + } + + let filename = &args[1]; + + // Parse the GEDCOM file + println!("Parsing GEDCOM file: {}", filename); + let gedcom = match parse_gedcom(filename, &GedcomConfig::new()) { + Ok(g) => g, + Err(e) => { + eprintln!("Error parsing GEDCOM file: {}", e); + process::exit(1); + } + }; + + println!( + "Successfully parsed {} individuals and {} families\n", + gedcom.individuals.len(), + gedcom.families.len() + ); + + // Determine which individual to examine + let person = if args.len() >= 3 { + let xref = &args[2]; + match gedcom.find_individual_by_xref(xref) { + Some(p) => p, + None => { + eprintln!("Error: Individual with ID {} not found", xref); + process::exit(1); + } + } + } else { + // Use first individual with family connections + gedcom + .individuals + .iter() + .find(|i| !i.famc.is_empty() || !i.fams.is_empty()) + .or_else(|| gedcom.individuals.first()) + .expect("No individuals found in GEDCOM file") + }; + + println!("=== Examining Relationships for ==="); + println!("Name: {}", get_name(person)); + println!("ID: {}", get_xref_str(person)); + println!("Gender: {:?}", person.gender); + if let Some(birth) = person.birth.first() { + if let Some(date) = &birth.event.detail.date { + println!("Birth: {}", date); + } + } + println!(); + + // Get parents + println!("=== Parents ==="); + let parents = gedcom.get_parents(person); + if parents.is_empty() { + println!("No parents found"); + } else { + for (i, (father, mother)) in parents.iter().enumerate() { + println!("Family {}:", i + 1); + if let Some(dad) = father { + println!(" Father: {} ({})", get_name(dad), get_xref_str(dad)); + if let Some(birth) = dad.birth.first() { + if let Some(date) = &birth.event.detail.date { + println!(" Birth: {}", date); + } + } + } else { + println!(" Father: (unknown)"); + } + + if let Some(mom) = mother { + println!(" Mother: {} ({})", get_name(mom), get_xref_str(mom)); + if let Some(birth) = mom.birth.first() { + if let Some(date) = &birth.event.detail.date { + println!(" Birth: {}", date); + } + } + } else { + println!(" Mother: (unknown)"); + } + } + } + println!(); + + // Get spouses + println!("=== Spouses ==="); + let spouses = gedcom.get_spouses(person); + if spouses.is_empty() { + println!("No spouses found"); + } else { + for (i, spouse) in spouses.iter().enumerate() { + println!("{}. {} ({})", i + 1, get_name(spouse), get_xref_str(spouse)); + if let Some(birth) = spouse.birth.first() { + if let Some(date) = &birth.event.detail.date { + println!(" Birth: {}", date); + } + } + } + } + println!(); + + // Get children + println!("=== Children ==="); + let children = gedcom.get_children(person); + if children.is_empty() { + println!("No children found"); + } else { + println!("Found {} child(ren):", children.len()); + for (i, child) in children.iter().enumerate() { + println!("{}. {} ({})", i + 1, get_name(child), get_xref_str(child)); + if let Some(birth) = child.birth.first() { + if let Some(date) = &birth.event.detail.date { + println!(" Birth: {}", date); + } + } + } + } + println!(); + + // Get siblings + println!("=== Siblings ==="); + let siblings = gedcom.get_siblings(person); + let full_siblings = gedcom.get_full_siblings(person); + let half_siblings = gedcom.get_half_siblings(person); + + if siblings.is_empty() { + println!("No siblings found"); + } else { + println!("Found {} total sibling(s):", siblings.len()); + println!(" - {} full sibling(s)", full_siblings.len()); + println!(" - {} half-sibling(s)", half_siblings.len()); + println!(); + + if !full_siblings.is_empty() { + println!("Full Siblings:"); + for (i, sibling) in full_siblings.iter().enumerate() { + println!( + " {}. {} ({})", + i + 1, + get_name(sibling), + get_xref_str(sibling) + ); + if let Some(birth) = sibling.birth.first() { + if let Some(date) = &birth.event.detail.date { + println!(" Birth: {}", date); + } + } + } + println!(); + } + + if !half_siblings.is_empty() { + println!("Half-Siblings:"); + for (i, sibling) in half_siblings.iter().enumerate() { + println!( + " {}. {} ({})", + i + 1, + get_name(sibling), + get_xref_str(sibling) + ); + if let Some(birth) = sibling.birth.first() { + if let Some(date) = &birth.event.detail.date { + println!(" Birth: {}", date); + } + } + } + } + } + println!(); + + // Summary + println!("=== Summary ==="); + println!( + "Total parents: {}", + parents + .iter() + .filter_map(|(f, m)| if f.is_some() || m.is_some() { + Some(1) + } else { + None + }) + .count() + ); + println!("Total spouses: {}", spouses.len()); + println!("Total children: {}", children.len()); + println!( + "Total siblings: {} ({} full, {} half)", + siblings.len(), + full_siblings.len(), + half_siblings.len() + ); +} diff --git a/examples/find_relationship.rs b/examples/find_relationship.rs new file mode 100644 index 0000000..debc23d --- /dev/null +++ b/examples/find_relationship.rs @@ -0,0 +1,242 @@ +//! Determine genealogical relationships between individuals +//! +//! This example demonstrates the find_relationship function which: +//! - Identifies the relationship type (parent, sibling, cousin, etc.) +//! - Finds the Most Recent Common Ancestor (MRCA) +//! - Calculates generational distances +//! +//! Usage: +//! cargo run --example find_relationship path/to/file.ged [xref1] [xref2] +//! +//! If no xrefs are provided, it will demonstrate relationships between +//! various individuals in the file. + +use gedcom_rs::parse::{parse_gedcom, GedcomConfig}; +use std::env; +use std::process; + +fn get_name(individual: &gedcom_rs::types::Individual) -> String { + individual + .names + .first() + .and_then(|n| n.name.value.as_ref()) + .map(|s| s.to_string()) + .unwrap_or_else(|| "(unnamed)".to_string()) +} + +fn get_xref_str(individual: &gedcom_rs::types::Individual) -> String { + individual + .xref + .as_ref() + .map(|x| x.to_string()) + .unwrap_or_else(|| "(no ID)".to_string()) +} + +fn print_relationship_details( + gedcom: &gedcom_rs::types::Gedcom, + person1: &gedcom_rs::types::Individual, + person2: &gedcom_rs::types::Individual, +) { + println!("=== Analyzing Relationship ==="); + println!( + "Person 1: {} ({})", + get_name(person1), + get_xref_str(person1) + ); + println!( + "Person 2: {} ({})", + get_name(person2), + get_xref_str(person2) + ); + println!(); + + let relationship = gedcom.find_relationship(person1, person2); + + println!("Relationship: {}", relationship.description); + + if let Some(gen1) = relationship.generations_to_mrca_1 { + println!("Generations from {} to MRCA: {}", get_name(person1), gen1); + } + if let Some(gen2) = relationship.generations_to_mrca_2 { + println!("Generations from {} to MRCA: {}", get_name(person2), gen2); + } + + if !relationship.mrca.is_empty() { + println!("\nMost Recent Common Ancestor(s):"); + for (i, ancestor) in relationship.mrca.iter().enumerate() { + println!( + " {}. {} ({})", + i + 1, + get_name(ancestor), + get_xref_str(ancestor) + ); + } + } else if relationship.description != "Not related" && relationship.description != "Self" { + println!("\nNo common ancestor (direct relationship)"); + } + println!(); +} + +fn main() { + // Get command line arguments + let args: Vec = env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: {} [xref1] [xref2]", args[0]); + eprintln!("\nDetermines the genealogical relationship between two individuals"); + eprintln!("\nExamples:"); + eprintln!(" cargo run --example find_relationship data/complete.ged"); + eprintln!(" cargo run --example find_relationship data/complete.ged @I1@ @I2@"); + process::exit(1); + } + + let filename = &args[1]; + + // Parse the GEDCOM file + println!("Parsing GEDCOM file: {}", filename); + let gedcom = match parse_gedcom(filename, &GedcomConfig::new()) { + Ok(g) => g, + Err(e) => { + eprintln!("Error parsing GEDCOM file: {}", e); + process::exit(1); + } + }; + + println!( + "Successfully parsed {} individuals and {} families\n", + gedcom.individuals.len(), + gedcom.families.len() + ); + + if gedcom.individuals.is_empty() { + eprintln!("No individuals found in GEDCOM file"); + process::exit(1); + } + + // If specific xrefs provided, use those + if args.len() >= 4 { + let xref1 = &args[2]; + let xref2 = &args[3]; + + let person1 = match gedcom.find_individual_by_xref(xref1) { + Some(p) => p, + None => { + eprintln!("Error: Individual with ID {} not found", xref1); + process::exit(1); + } + }; + + let person2 = match gedcom.find_individual_by_xref(xref2) { + Some(p) => p, + None => { + eprintln!("Error: Individual with ID {} not found", xref2); + process::exit(1); + } + }; + + print_relationship_details(&gedcom, person1, person2); + } else { + // Demonstrate various relationships in the file + println!("=== Demonstrating Various Relationships ===\n"); + + // Find some interesting relationships to demonstrate + let mut demonstrated = 0; + + // Try to find parent-child relationships + for person in gedcom.individuals.iter().take(10) { + let children = gedcom.get_children(person); + if !children.is_empty() { + println!( + "--- Example {}: Parent-Child Relationship ---", + demonstrated + 1 + ); + print_relationship_details(&gedcom, person, children[0]); + demonstrated += 1; + if demonstrated >= 5 { + break; + } + } + } + + // Try to find sibling relationships + if demonstrated < 5 { + for person in gedcom.individuals.iter().take(10) { + let siblings = gedcom.get_siblings(person); + if !siblings.is_empty() { + println!("--- Example {}: Sibling Relationship ---", demonstrated + 1); + print_relationship_details(&gedcom, person, siblings[0]); + demonstrated += 1; + if demonstrated >= 5 { + break; + } + } + } + } + + // Try to find cousin relationships + if demonstrated < 5 { + for person in gedcom.individuals.iter().take(20) { + // Get all descendants of grandparents to find cousins + for (gf, gm) in gedcom.get_parents(person) { + for grandparent in [gf, gm].iter().filter_map(|x| *x) { + let descendants = gedcom.get_descendants(grandparent, Some(3)); + for descendant in descendants.iter() { + let relationship = gedcom.find_relationship(person, descendant); + if relationship.description.contains("Cousin") + || relationship.description.contains("Aunt") + || relationship.description.contains("Uncle") + { + println!( + "--- Example {}: {} ---", + demonstrated + 1, + relationship.description + ); + print_relationship_details(&gedcom, person, descendant); + demonstrated += 1; + if demonstrated >= 5 { + break; + } + } + } + if demonstrated >= 5 { + break; + } + } + if demonstrated >= 5 { + break; + } + } + if demonstrated >= 5 { + break; + } + } + } + + // Try to find spouse relationships + if demonstrated < 5 { + for person in gedcom.individuals.iter().take(10) { + let spouses = gedcom.get_spouses(person); + if !spouses.is_empty() { + println!("--- Example {}: Spouse Relationship ---", demonstrated + 1); + print_relationship_details(&gedcom, person, spouses[0]); + demonstrated += 1; + if demonstrated >= 5 { + break; + } + } + } + } + + if demonstrated == 0 { + println!("Could not find interesting relationships to demonstrate."); + println!("Try specifying two individual IDs to compare."); + } else { + println!("=== Summary ==="); + println!("Demonstrated {} different relationship types", demonstrated); + println!("\nTip: Run with two specific XREFs to analyze any relationship:"); + println!( + " cargo run --example find_relationship {} @I1@ @I2@", + filename + ); + } + } +} diff --git a/examples/search_by_date.rs b/examples/search_by_date.rs new file mode 100644 index 0000000..eb823b7 --- /dev/null +++ b/examples/search_by_date.rs @@ -0,0 +1,165 @@ +//! Search for individuals by event date +//! +//! This example demonstrates the find_individuals_by_event_date function +//! which allows searching for individuals based on specific life events and dates. +//! +//! Usage: +//! cargo run --example search_by_date +//! +//! Event types: Birth, Death, Christening, Marriage +//! Date pattern: Any partial date string (e.g., "1965", "MAR 1999", "DEC") + +use gedcom_rs::parse::{parse_gedcom, GedcomConfig}; +use gedcom_rs::types::EventType; +use std::env; +use std::process; + +fn get_name(individual: &gedcom_rs::types::Individual) -> String { + individual + .names + .first() + .and_then(|n| n.name.value.as_ref()) + .map(|s| s.to_string()) + .unwrap_or_else(|| "(unnamed)".to_string()) +} + +fn get_xref_str(individual: &gedcom_rs::types::Individual) -> String { + individual + .xref + .as_ref() + .map(|x| x.to_string()) + .unwrap_or_else(|| "(no ID)".to_string()) +} + +fn parse_event_type(s: &str) -> Option { + match s.to_lowercase().as_str() { + "birth" => Some(EventType::Birth), + "death" => Some(EventType::Death), + "christening" => Some(EventType::Christening), + "baptism" => Some(EventType::Baptism), + "burial" => Some(EventType::Burial), + "adoption" => Some(EventType::Adoption), + "census" => Some(EventType::Census), + "emigration" => Some(EventType::Emigration), + "immigration" => Some(EventType::Immigration), + _ => None, + } +} + +fn main() { + // Get command line arguments + let args: Vec = env::args().collect(); + if args.len() < 4 { + eprintln!( + "Usage: {} ", + args[0] + ); + eprintln!("\nSearches for individuals by event date"); + eprintln!("\nEvent types:"); + eprintln!(" Birth, Death, Christening, Baptism, Burial"); + eprintln!(" Adoption, Census, Emigration, Immigration"); + eprintln!("\nExamples:"); + eprintln!(" cargo run --example search_by_date data/complete.ged Birth 1965"); + eprintln!(" cargo run --example search_by_date data/complete.ged Death \"DEC 1997\""); + eprintln!(" cargo run --example search_by_date data/complete.ged Christening MAR"); + process::exit(1); + } + + let filename = &args[1]; + let event_type_str = &args[2]; + let date_pattern = &args[3]; + + // Parse event type + let event_type = match parse_event_type(event_type_str) { + Some(et) => et, + None => { + eprintln!("Error: Unknown event type '{}'", event_type_str); + eprintln!("Supported: Birth, Death, Christening, Baptism, Burial, Adoption, Census, Emigration, Immigration"); + process::exit(1); + } + }; + + // Parse the GEDCOM file + println!("Parsing GEDCOM file: {}", filename); + let gedcom = match parse_gedcom(filename, &GedcomConfig::new()) { + Ok(g) => g, + Err(e) => { + eprintln!("Error parsing GEDCOM file: {}", e); + process::exit(1); + } + }; + + println!( + "Successfully parsed {} individuals\n", + gedcom.individuals.len() + ); + + // Search for individuals + println!( + "=== Searching for {:?} events matching '{}' ===\n", + event_type, date_pattern + ); + let results = gedcom.find_individuals_by_event_date(event_type, date_pattern); + + if results.is_empty() { + println!( + "No individuals found with {:?} events matching '{}'", + event_type, date_pattern + ); + } else { + println!("Found {} individual(s):\n", results.len()); + for (i, person) in results.iter().enumerate() { + println!("{}. {} ({})", i + 1, get_name(person), get_xref_str(person)); + + // Show the matching event date + match event_type { + EventType::Birth => { + for birth in &person.birth { + if let Some(date) = &birth.event.detail.date { + if date.to_lowercase().contains(&date_pattern.to_lowercase()) { + println!(" Birth: {}", date); + if let Some(place) = &birth.event.detail.place { + if let Some(place_name) = &place.name { + println!(" Place: {}", place_name); + } + } + } + } + } + } + EventType::Death => { + for death in &person.death { + if let Some(event) = &death.event { + if let Some(date) = &event.date { + if date.to_lowercase().contains(&date_pattern.to_lowercase()) { + println!(" Death: {}", date); + if let Some(place) = &event.place { + if let Some(place_name) = &place.name { + println!(" Place: {}", place_name); + } + } + } + } + } + } + } + EventType::Christening => { + for christening in &person.christening { + if let Some(date) = &christening.event.detail.date { + if date.to_lowercase().contains(&date_pattern.to_lowercase()) { + println!(" Christening: {}", date); + if let Some(place) = &christening.event.detail.place { + if let Some(place_name) = &place.name { + println!(" Place: {}", place_name); + } + } + } + } + } + } + _ => {} // Handle other event types as needed + } + println!(); + } + } +} diff --git a/examples/search_individuals.rs b/examples/search_individuals.rs new file mode 100644 index 0000000..c2bbf4b --- /dev/null +++ b/examples/search_individuals.rs @@ -0,0 +1,169 @@ +//! Search for individuals in a GEDCOM file using various methods +//! +//! This example demonstrates the search functions available on the Gedcom struct: +//! - find_individual_by_xref: Find a specific individual by their ID +//! - find_individuals_by_name: Search for individuals by name (partial match) +//! - find_family_by_xref: Find a specific family by its ID +//! +//! Usage: +//! cargo run --example search_individuals path/to/file.ged + +use gedcom_rs::parse::{parse_gedcom, GedcomConfig}; +use std::env; +use std::process; + +fn print_individual_summary(individual: &gedcom_rs::types::Individual) { + // Display name + if let Some(name) = individual.names.first() { + if let Some(value) = &name.name.value { + println!(" Name: {}", value); + } + } + + // Display reference ID + if let Some(xref) = &individual.xref { + println!(" ID: {}", xref); + } + + // Display gender + println!(" Gender: {:?}", individual.gender); + + // Display birth date + if let Some(birth) = individual.birth.first() { + if let Some(date) = &birth.event.detail.date { + println!(" Birth: {}", date); + } + } + + // Display death date + if let Some(death) = individual.death.first() { + if let Some(event) = &death.event { + if let Some(date) = &event.date { + println!(" Death: {}", date); + } + } + } +} + +fn main() { + // Get filename from command line arguments + let args: Vec = env::args().collect(); + if args.len() != 2 { + eprintln!("Usage: {} ", args[0]); + eprintln!("\nDemonstrates various search functions on a GEDCOM file"); + eprintln!("\nExample:"); + eprintln!(" cargo run --example search_individuals data/TGC551.ged"); + process::exit(1); + } + + let filename = &args[1]; + + // Parse the GEDCOM file + println!("Parsing GEDCOM file: {}", filename); + let gedcom = match parse_gedcom(filename, &GedcomConfig::new()) { + Ok(g) => g, + Err(e) => { + eprintln!("Error parsing GEDCOM file: {}", e); + process::exit(1); + } + }; + + println!( + "Successfully parsed {} individuals and {} families\n", + gedcom.individuals.len(), + gedcom.families.len() + ); + + // Example 1: Find individual by xref + println!("=== Example 1: Find Individual by XREF ==="); + if let Some(first_person) = gedcom.individuals.first() { + if let Some(xref) = &first_person.xref { + println!("Searching for individual with ID: {}", xref); + if let Some(person) = gedcom.find_individual_by_xref(xref.as_str()) { + println!("Found:"); + print_individual_summary(person); + } + } + } + println!(); + + // Example 2: Find individuals by name (partial match) + println!("=== Example 2: Find Individuals by Name ==="); + // Try to search for a common name component + let search_terms = vec!["Smith", "John", "Mary", "William"]; + + for search_term in &search_terms { + let results = gedcom.find_individuals_by_name(search_term); + if !results.is_empty() { + println!( + "Found {} individual(s) with name containing '{}':", + results.len(), + search_term + ); + for person in results.iter().take(3) { + // Show first 3 matches + print_individual_summary(person); + println!(); + } + if results.len() > 3 { + println!(" ... and {} more\n", results.len() - 3); + } + break; // Found results, stop searching + } + } + println!(); + + // Example 3: Find family by xref + println!("=== Example 3: Find Family by XREF ==="); + if let Some(first_family) = gedcom.families.first() { + let family_xref = first_family.xref.as_str(); + println!("Searching for family with ID: {}", family_xref); + if let Some(family) = gedcom.find_family_by_xref(family_xref) { + println!("Found family:"); + if let Some(husband) = &family.husband { + if let Some(h) = gedcom.find_individual_by_xref(husband.as_str()) { + if let Some(name) = h.names.first() { + if let Some(value) = &name.name.value { + println!(" Husband: {}", value); + } + } + } + } + if let Some(wife) = &family.wife { + if let Some(w) = gedcom.find_individual_by_xref(wife.as_str()) { + if let Some(name) = w.names.first() { + if let Some(value) = &name.name.value { + println!(" Wife: {}", value); + } + } + } + } + println!(" Children: {}", family.children.len()); + } + } + println!(); + + // Example 4: Case-insensitive name search + println!("=== Example 4: Case-Insensitive Search ==="); + if let Some(first_person) = gedcom.individuals.first() { + if let Some(name) = first_person.names.first() { + if let Some(value) = &name.name.value { + // Search with different cases + let lower = value.to_lowercase(); + let upper = value.to_uppercase(); + + println!("Original name: {}", value); + println!("Searching with lowercase: {}", lower); + let results_lower = gedcom.find_individuals_by_name(&lower); + println!(" Found {} result(s)", results_lower.len()); + + println!("Searching with uppercase: {}", upper); + let results_upper = gedcom.find_individuals_by_name(&upper); + println!(" Found {} result(s)", results_upper.len()); + } + } + } + println!(); + + println!("Search examples complete!"); +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 000f735..e2866fb 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -57,6 +57,95 @@ pub use submission::Submission; pub use submitter::Submitter; pub use xref::Xref; +/// Describes the genealogical relationship between two individuals +#[derive(Debug, Clone)] +pub struct RelationshipResult<'a> { + /// Human-readable description of the relationship + /// Examples: "Parent", "Child", "Sibling", "First Cousin", "Second Cousin Once Removed" + pub description: String, + + /// Most Recent Common Ancestor(s) - the shared ancestor(s) closest to the individuals + /// For siblings, this would be their parents + /// For first cousins, this would be their grandparents + /// Can be empty if no common ancestor is found (not related) + pub mrca: Vec<&'a Individual>, + + /// Distance from person1 to MRCA (generations up) + pub generations_to_mrca_1: Option, + + /// Distance from person2 to MRCA (generations up) + pub generations_to_mrca_2: Option, +} + +impl<'a> RelationshipResult<'a> { + /// Create a new relationship result with no relationship found + pub fn none() -> Self { + RelationshipResult { + description: "Not related".to_string(), + mrca: Vec::new(), + generations_to_mrca_1: None, + generations_to_mrca_2: None, + } + } + + /// Create a new relationship result for self (same person) + pub fn self_relation() -> Self { + RelationshipResult { + description: "Self".to_string(), + mrca: Vec::new(), + generations_to_mrca_1: Some(0), + generations_to_mrca_2: Some(0), + } + } +} + +/// Event types that can be searched for individuals +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EventType { + /// Birth event + Birth, + /// Death event + Death, + /// Christening event + Christening, + /// Adult christening event + ChristeningAdult, + /// Baptism event + Baptism, + /// Bar Mitzvah event + BarMitzvah, + /// Bas Mitzvah event + BasMitzvah, + /// Blessing event + Blessing, + /// Burial event + Burial, + /// Census event + Census, + /// Confirmation event + Confirmation, + /// First Communion event + FirstCommunion, + /// Cremation event + Cremation, + /// Adoption event + Adoption, + /// Emigration event + Emigration, + /// Graduation event + Graduation, + /// Immigration event + Immigration, + /// Naturalization event + Naturalization, + /// Probate event + Probate, + /// Retirement event + Retirement, + /// Will event + Will, +} + #[derive(Debug, Default)] pub struct Gedcom { pub header: Header, @@ -68,3 +157,1307 @@ pub struct Gedcom { pub multimedia: Vec, pub submitters: Vec, } + +impl Gedcom { + // ===== Search Functions ===== + + /// Find an individual by their cross-reference ID (xref) + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// if let Some(person) = gedcom.find_individual_by_xref("@I1@") { + /// println!("Found person: {:?}", person.names); + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn find_individual_by_xref(&self, xref: &str) -> Option<&Individual> { + self.individuals + .iter() + .find(|indi| indi.xref.as_ref().map(|x| x.as_str()) == Some(xref)) + } + + /// Find individuals by name (partial or full match, case-insensitive) + /// + /// Returns all individuals whose name contains the search string. + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// let results = gedcom.find_individuals_by_name("Smith"); + /// for person in results { + /// println!("Found: {:?}", person.names); + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn find_individuals_by_name(&self, name: &str) -> Vec<&Individual> { + let search_lower = name.to_lowercase(); + self.individuals + .iter() + .filter(|indi| { + indi.names.iter().any(|n| { + n.name + .value + .as_ref() + .map(|v| v.to_lowercase().contains(&search_lower)) + .unwrap_or(false) + }) + }) + .collect() + } + + /// Find a family record by its cross-reference ID (xref) + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// if let Some(family) = gedcom.find_family_by_xref("@F1@") { + /// println!("Found family with {} children", family.children.len()); + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn find_family_by_xref(&self, xref: &str) -> Option<&Family> { + self.families.iter().find(|fam| fam.xref.as_str() == xref) + } + + /// Find individuals by event date + /// + /// Returns all individuals who have an event of the specified type + /// that contains the given date string (partial match, case-insensitive). + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// use gedcom_rs::types::EventType; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// // Find all individuals born in 1965 + /// let results = gedcom.find_individuals_by_event_date(EventType::Birth, "1965"); + /// for person in results { + /// println!("Found: {:?}", person.names); + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn find_individuals_by_event_date( + &self, + event_type: EventType, + date_pattern: &str, + ) -> Vec<&Individual> { + let pattern_lower = date_pattern.to_lowercase(); + + self.individuals + .iter() + .filter(|individual| { + let has_matching_date = + match event_type { + EventType::Birth => individual.birth.iter().any(|e| { + Self::event_date_matches(&e.event.detail.date, &pattern_lower) + }), + EventType::Death => individual.death.iter().any(|e| { + e.event + .as_ref() + .and_then(|ev| ev.date.as_ref()) + .map(|d| d.to_lowercase().contains(&pattern_lower)) + .unwrap_or(false) + }), + EventType::Christening => individual.christening.iter().any(|e| { + Self::event_date_matches(&e.event.detail.date, &pattern_lower) + }), + EventType::ChristeningAdult => individual + .christening_adult + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.event.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Baptism => individual + .baptism + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::BarMitzvah => individual + .barmitzvah + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::BasMitzvah => individual + .basmitzvah + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Blessing => individual + .blessing + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Burial => individual + .burial + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Census => individual + .census + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Confirmation => individual + .confirmation + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::FirstCommunion => individual + .first_communion + .as_ref() + .map(|e| Self::event_date_matches(&e.detail.date, &pattern_lower)) + .unwrap_or(false), + EventType::Cremation => individual + .cremation + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Adoption => individual + .adoption + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.event.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Emigration => individual + .emigration + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Graduation => individual + .graduation + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Immigration => individual + .immigration + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Naturalization => individual + .naturalization + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Probate => individual + .probate + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Retirement => individual + .retirement + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + EventType::Will => individual + .will + .as_ref() + .map(|events| { + events.iter().any(|e| { + Self::event_date_matches(&e.detail.date, &pattern_lower) + }) + }) + .unwrap_or(false), + }; + + has_matching_date + }) + .collect() + } + + /// Helper function to check if an event date matches a pattern + fn event_date_matches(date_opt: &Option, pattern_lower: &str) -> bool { + date_opt + .as_ref() + .map(|d| d.to_lowercase().contains(pattern_lower)) + .unwrap_or(false) + } + + // ===== Basic Relationship Functions ===== + + /// Get the parents of an individual + /// + /// Returns a vector of tuples containing (father, mother) for each family + /// the individual is a child in. Either parent may be None. + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// if let Some(person) = gedcom.find_individual_by_xref("@I1@") { + /// let parents = gedcom.get_parents(person); + /// for (father, mother) in parents { + /// if let Some(dad) = father { + /// println!("Father: {:?}", dad.names); + /// } + /// if let Some(mom) = mother { + /// println!("Mother: {:?}", mom.names); + /// } + /// } + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn get_parents( + &self, + individual: &Individual, + ) -> Vec<(Option<&Individual>, Option<&Individual>)> { + individual + .famc + .iter() + .filter_map(|fam_link| { + self.find_family_by_xref(fam_link.xref.as_str()) + .map(|family| { + let father = family + .husband + .as_ref() + .and_then(|xref| self.find_individual_by_xref(xref.as_str())); + let mother = family + .wife + .as_ref() + .and_then(|xref| self.find_individual_by_xref(xref.as_str())); + (father, mother) + }) + }) + .collect() + } + + /// Get the children of an individual + /// + /// Returns all children from all families where this individual is a spouse. + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// if let Some(person) = gedcom.find_individual_by_xref("@I1@") { + /// let children = gedcom.get_children(person); + /// println!("{} has {} children", person.xref.as_ref().unwrap(), children.len()); + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn get_children(&self, individual: &Individual) -> Vec<&Individual> { + individual + .fams + .iter() + .filter_map(|fam_link| self.find_family_by_xref(fam_link.xref.as_str())) + .flat_map(|family| { + family + .children + .iter() + .filter_map(|child_xref| self.find_individual_by_xref(child_xref.as_str())) + }) + .collect() + } + + /// Get the spouses of an individual + /// + /// Returns all spouses from all families where this individual is listed. + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// if let Some(person) = gedcom.find_individual_by_xref("@I1@") { + /// let spouses = gedcom.get_spouses(person); + /// for spouse in spouses { + /// println!("Spouse: {:?}", spouse.names); + /// } + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn get_spouses(&self, individual: &Individual) -> Vec<&Individual> { + let individual_xref = match &individual.xref { + Some(xref) => xref.as_str(), + None => return Vec::new(), + }; + + individual + .fams + .iter() + .filter_map(|fam_link| self.find_family_by_xref(fam_link.xref.as_str())) + .filter_map(|family| { + // If this individual is the husband, return the wife + if family.husband.as_ref().map(|x| x.as_str()) == Some(individual_xref) { + family + .wife + .as_ref() + .and_then(|xref| self.find_individual_by_xref(xref.as_str())) + } + // If this individual is the wife, return the husband + else if family.wife.as_ref().map(|x| x.as_str()) == Some(individual_xref) { + family + .husband + .as_ref() + .and_then(|xref| self.find_individual_by_xref(xref.as_str())) + } else { + None + } + }) + .collect() + } + + /// Get siblings of an individual + /// + /// Returns all siblings (individuals who share at least one parent). + /// This includes both full siblings and half-siblings, even if they + /// are in different family records. + /// Does not include the individual themselves. + /// + /// For more specific queries, see: + /// - `get_full_siblings()` - only full siblings (same mother and father) + /// - `get_half_siblings()` - only half-siblings (one shared parent) + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// if let Some(person) = gedcom.find_individual_by_xref("@I1@") { + /// let siblings = gedcom.get_siblings(person); + /// println!("Found {} siblings", siblings.len()); + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn get_siblings(&self, individual: &Individual) -> Vec<&Individual> { + let individual_xref = match &individual.xref { + Some(xref) => xref.as_str(), + None => return Vec::new(), + }; + + // Get all parents + let parents = self.get_parents(individual); + let mut sibling_xrefs = std::collections::HashSet::new(); + + // For each parent, find all their children (who are this individual's siblings) + for (father, mother) in parents { + // Get all families where father is the husband + if let Some(dad) = father { + if let Some(dad_xref) = &dad.xref { + for family in &self.families { + if family.husband.as_ref().map(|x| x.as_str()) == Some(dad_xref.as_str()) { + for child_xref in &family.children { + if child_xref.as_str() != individual_xref { + sibling_xrefs.insert(child_xref.as_str()); + } + } + } + } + } + } + + // Get all families where mother is the wife + if let Some(mom) = mother { + if let Some(mom_xref) = &mom.xref { + for family in &self.families { + if family.wife.as_ref().map(|x| x.as_str()) == Some(mom_xref.as_str()) { + for child_xref in &family.children { + if child_xref.as_str() != individual_xref { + sibling_xrefs.insert(child_xref.as_str()); + } + } + } + } + } + } + } + + // Convert xrefs to Individual references + sibling_xrefs + .into_iter() + .filter_map(|xref| self.find_individual_by_xref(xref)) + .collect() + } + + /// Get full siblings of an individual + /// + /// Returns only full siblings (individuals who share both parents). + /// Does not include the individual themselves. + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// if let Some(person) = gedcom.find_individual_by_xref("@I1@") { + /// let full_siblings = gedcom.get_full_siblings(person); + /// println!("Found {} full siblings", full_siblings.len()); + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn get_full_siblings(&self, individual: &Individual) -> Vec<&Individual> { + let individual_xref = match &individual.xref { + Some(xref) => xref.as_str(), + None => return Vec::new(), + }; + + // Get all families where this individual is a child + let parent_families: Vec<_> = individual + .famc + .iter() + .filter_map(|fam_link| self.find_family_by_xref(fam_link.xref.as_str())) + .collect(); + + // Collect xrefs of full siblings to deduplicate + let mut full_sibling_xrefs = std::collections::HashSet::new(); + + // If individual has no families or only one family, check that one family + // Full siblings must come from the same family (same mother and father) + for family in &parent_families { + // Get both parents from this family + let has_both_parents = family.husband.is_some() && family.wife.is_some(); + + if !has_both_parents { + continue; + } + + for child_xref in &family.children { + // Exclude self + if child_xref.as_str() == individual_xref { + continue; + } + + // Check if this sibling appears in ALL the same parent families + // For full siblings, they must share all parent families + if let Some(sibling) = self.find_individual_by_xref(child_xref.as_str()) { + let sibling_families: Vec<_> = sibling + .famc + .iter() + .filter_map(|fam_link| self.find_family_by_xref(fam_link.xref.as_str())) + .collect(); + + // Full siblings share the exact same set of parent families + if parent_families.len() == sibling_families.len() + && parent_families + .iter() + .all(|pf| sibling_families.iter().any(|sf| std::ptr::eq(*pf, *sf))) + { + full_sibling_xrefs.insert(child_xref.as_str()); + } + } + } + } + + // Convert xrefs back to Individual references + full_sibling_xrefs + .into_iter() + .filter_map(|xref| self.find_individual_by_xref(xref)) + .collect() + } + + /// Get half-siblings of an individual + /// + /// Returns only half-siblings (individuals who share exactly one parent). + /// Does not include the individual themselves or full siblings. + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// if let Some(person) = gedcom.find_individual_by_xref("@I1@") { + /// let half_siblings = gedcom.get_half_siblings(person); + /// println!("Found {} half-siblings", half_siblings.len()); + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn get_half_siblings(&self, individual: &Individual) -> Vec<&Individual> { + let all_siblings = self.get_siblings(individual); + let full_siblings = self.get_full_siblings(individual); + + // Convert full_siblings to a set for efficient lookup + let full_sibling_xrefs: std::collections::HashSet<_> = full_siblings + .iter() + .filter_map(|s| s.xref.as_ref().map(|x| x.as_str())) + .collect(); + + // Filter out full siblings from all siblings + all_siblings + .into_iter() + .filter(|sibling| { + sibling + .xref + .as_ref() + .map(|x| !full_sibling_xrefs.contains(x.as_str())) + .unwrap_or(false) + }) + .collect() + } + + // ===== Advanced Relationship Functions ===== + + /// Get all ancestors of an individual up to a specified number of generations + /// + /// Returns a vector of individuals representing all ancestors. If max_generations + /// is None, it will traverse all generations until no more ancestors are found. + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// if let Some(person) = gedcom.find_individual_by_xref("@I1@") { + /// // Get all ancestors up to 5 generations + /// let ancestors = gedcom.get_ancestors(person, Some(5)); + /// println!("Found {} ancestors", ancestors.len()); + /// + /// // Get ALL ancestors + /// let all_ancestors = gedcom.get_ancestors(person, None); + /// println!("Found {} total ancestors", all_ancestors.len()); + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn get_ancestors( + &self, + individual: &Individual, + max_generations: Option, + ) -> Vec<&Individual> { + use std::collections::{HashSet, VecDeque}; + + let mut ancestors = Vec::new(); + let mut visited = HashSet::new(); + let mut queue = VecDeque::new(); + + // Add the individual's xref to visited to avoid cycles + if let Some(xref) = &individual.xref { + visited.insert(xref.as_str()); + } + + // Queue: (individual, generation) + queue.push_back((individual, 0)); + + while let Some((current, generation)) = queue.pop_front() { + // Check if we've reached the max generation limit + if let Some(max_gen) = max_generations { + if generation >= max_gen { + continue; + } + } + + // Get parents + let parents = self.get_parents(current); + for (father, mother) in parents { + if let Some(dad) = father { + if let Some(dad_xref) = &dad.xref { + if visited.insert(dad_xref.as_str()) { + ancestors.push(dad); + queue.push_back((dad, generation + 1)); + } + } + } + if let Some(mom) = mother { + if let Some(mom_xref) = &mom.xref { + if visited.insert(mom_xref.as_str()) { + ancestors.push(mom); + queue.push_back((mom, generation + 1)); + } + } + } + } + } + + ancestors + } + + /// Get all descendants of an individual up to a specified number of generations + /// + /// Returns a vector of individuals representing all descendants. If max_generations + /// is None, it will traverse all generations until no more descendants are found. + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// if let Some(person) = gedcom.find_individual_by_xref("@I1@") { + /// // Get all descendants up to 3 generations + /// let descendants = gedcom.get_descendants(person, Some(3)); + /// println!("Found {} descendants", descendants.len()); + /// + /// // Get ALL descendants + /// let all_descendants = gedcom.get_descendants(person, None); + /// println!("Found {} total descendants", all_descendants.len()); + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn get_descendants( + &self, + individual: &Individual, + max_generations: Option, + ) -> Vec<&Individual> { + use std::collections::{HashSet, VecDeque}; + + let mut descendants = Vec::new(); + let mut visited = HashSet::new(); + let mut queue = VecDeque::new(); + + // Add the individual's xref to visited to avoid cycles + if let Some(xref) = &individual.xref { + visited.insert(xref.as_str()); + } + + // Queue: (individual, generation) + queue.push_back((individual, 0)); + + while let Some((current, generation)) = queue.pop_front() { + // Check if we've reached the max generation limit + if let Some(max_gen) = max_generations { + if generation >= max_gen { + continue; + } + } + + // Get children + let children = self.get_children(current); + for child in children { + if let Some(child_xref) = &child.xref { + if visited.insert(child_xref.as_str()) { + descendants.push(child); + queue.push_back((child, generation + 1)); + } + } + } + } + + descendants + } + + /// Find the relationship path between two individuals + /// + /// Returns the shortest path between two individuals, or None if they are not related. + /// The path includes both individuals and all connecting individuals. + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// if let (Some(person1), Some(person2)) = ( + /// gedcom.find_individual_by_xref("@I1@"), + /// gedcom.find_individual_by_xref("@I2@") + /// ) { + /// if let Some(path) = gedcom.find_relationship_path(person1, person2) { + /// println!("Relationship path has {} people", path.len()); + /// for person in path { + /// println!(" - {:?}", person.names); + /// } + /// } + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn find_relationship_path<'a>( + &'a self, + from: &'a Individual, + to: &'a Individual, + ) -> Option> { + use std::collections::{HashMap, HashSet, VecDeque}; + + let from_xref = from.xref.as_ref()?.as_str(); + let to_xref = to.xref.as_ref()?.as_str(); + + if from_xref == to_xref { + return Some(vec![from]); + } + + let mut queue = VecDeque::new(); + let mut visited = HashSet::new(); + let mut parent_map: HashMap<&str, &str> = HashMap::new(); + + queue.push_back(from); + visited.insert(from_xref); + + while let Some(current) = queue.pop_front() { + let current_xref = current.xref.as_ref()?.as_str(); + + // Get all related individuals (parents, children, spouses, siblings) + let mut related = Vec::new(); + + // Add parents + for (father, mother) in self.get_parents(current) { + if let Some(dad) = father { + related.push(dad); + } + if let Some(mom) = mother { + related.push(mom); + } + } + + // Add children + related.extend(self.get_children(current)); + + // Add spouses + related.extend(self.get_spouses(current)); + + // Add siblings + related.extend(self.get_siblings(current)); + + for person in related { + let person_xref = person.xref.as_ref()?.as_str(); + + if !visited.contains(person_xref) { + visited.insert(person_xref); + parent_map.insert(person_xref, current_xref); + queue.push_back(person); + + // Found the target + if person_xref == to_xref { + // Reconstruct path + let mut path = vec![to]; + let mut current_key = to_xref; + + while let Some(&parent_key) = parent_map.get(current_key) { + if let Some(parent_person) = self.find_individual_by_xref(parent_key) { + path.push(parent_person); + } + current_key = parent_key; + } + + path.reverse(); + return Some(path); + } + } + } + } + + None + } + + /// Determine the genealogical relationship between two individuals + /// + /// Returns a RelationshipResult containing a human-readable description of the relationship + /// and the Most Recent Common Ancestor(s) (MRCA). + /// + /// # Relationship Types Detected + /// + /// - Direct: Parent, Child, Grandparent, Great-Grandparent, etc. + /// - Sibling relationships: Sibling, Half-Sibling + /// - Spouse + /// - Aunt/Uncle, Niece/Nephew, Grand-Aunt/Grand-Uncle, etc. + /// - Cousins: 1st Cousin, 2nd Cousin, 3rd Cousin, etc. + /// - Removed cousins: 1st Cousin 1x Removed, 2nd Cousin 2x Removed, etc. + /// + /// # Examples + /// + /// ```no_run + /// use gedcom_rs::parse::parse_gedcom; + /// use gedcom_rs::parse::GedcomConfig; + /// + /// let gedcom = parse_gedcom("file.ged", &GedcomConfig::new())?; + /// if let (Some(person1), Some(person2)) = ( + /// gedcom.find_individual_by_xref("@I1@"), + /// gedcom.find_individual_by_xref("@I2@") + /// ) { + /// let relationship = gedcom.find_relationship(person1, person2); + /// println!("Relationship: {}", relationship.description); + /// if !relationship.mrca.is_empty() { + /// println!("Common ancestor(s): {} found", relationship.mrca.len()); + /// } + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn find_relationship<'a>( + &'a self, + person1: &'a Individual, + person2: &'a Individual, + ) -> RelationshipResult<'a> { + // Get xrefs for comparison + let xref1 = match &person1.xref { + Some(x) => x.as_str(), + None => return RelationshipResult::none(), + }; + let xref2 = match &person2.xref { + Some(x) => x.as_str(), + None => return RelationshipResult::none(), + }; + + // Check if same person + if xref1 == xref2 { + return RelationshipResult::self_relation(); + } + + // Check for spouse relationship + if self + .get_spouses(person1) + .iter() + .any(|s| s.xref.as_ref().map(|x| x.as_str()) == Some(xref2)) + { + return RelationshipResult { + description: "Spouse".to_string(), + mrca: Vec::new(), + generations_to_mrca_1: None, + generations_to_mrca_2: None, + }; + } + + // Check for parent/child relationship + for (father, mother) in self.get_parents(person1) { + if father.and_then(|f| f.xref.as_ref()).map(|x| x.as_str()) == Some(xref2) { + return RelationshipResult { + description: "Father".to_string(), + mrca: Vec::new(), + generations_to_mrca_1: Some(1), + generations_to_mrca_2: Some(0), + }; + } + if mother.and_then(|m| m.xref.as_ref()).map(|x| x.as_str()) == Some(xref2) { + return RelationshipResult { + description: "Mother".to_string(), + mrca: Vec::new(), + generations_to_mrca_1: Some(1), + generations_to_mrca_2: Some(0), + }; + } + } + + // Check if person2 is parent of person1 (inverse) + for (father, mother) in self.get_parents(person2) { + if father.and_then(|f| f.xref.as_ref()).map(|x| x.as_str()) == Some(xref1) { + return RelationshipResult { + description: "Son".to_string(), + mrca: Vec::new(), + generations_to_mrca_1: Some(0), + generations_to_mrca_2: Some(1), + }; + } + if mother.and_then(|m| m.xref.as_ref()).map(|x| x.as_str()) == Some(xref1) { + return RelationshipResult { + description: "Daughter".to_string(), + mrca: Vec::new(), + generations_to_mrca_1: Some(0), + generations_to_mrca_2: Some(1), + }; + } + } + + // Check for sibling relationship + let siblings1 = self.get_siblings(person1); + if siblings1 + .iter() + .any(|s| s.xref.as_ref().map(|x| x.as_str()) == Some(xref2)) + { + // Get parents to determine if full or half siblings + let parents1: Vec<_> = self.get_parents(person1).into_iter().collect(); + let parents2: Vec<_> = self.get_parents(person2).into_iter().collect(); + + let mut common_parents = Vec::new(); + for (f1, m1) in &parents1 { + for (f2, m2) in &parents2 { + if let (Some(father1), Some(father2)) = (f1, f2) { + if father1.xref.as_ref() == father2.xref.as_ref() { + if !common_parents + .iter() + .any(|p: &&Individual| p.xref.as_ref() == father1.xref.as_ref()) + { + common_parents.push(*father1); + } + } + } + if let (Some(mother1), Some(mother2)) = (m1, m2) { + if mother1.xref.as_ref() == mother2.xref.as_ref() { + if !common_parents + .iter() + .any(|p: &&Individual| p.xref.as_ref() == mother1.xref.as_ref()) + { + common_parents.push(*mother1); + } + } + } + } + } + + let description = if common_parents.len() >= 2 { + "Sibling".to_string() + } else { + "Half-Sibling".to_string() + }; + + return RelationshipResult { + description, + mrca: common_parents, + generations_to_mrca_1: Some(1), + generations_to_mrca_2: Some(1), + }; + } + + // Find MRCA and calculate cousin/removed relationships + self.find_relationship_via_mrca(person1, person2) + } + + /// Helper function to find relationship through Most Recent Common Ancestor + fn find_relationship_via_mrca<'a>( + &'a self, + person1: &'a Individual, + person2: &'a Individual, + ) -> RelationshipResult<'a> { + use std::collections::{HashMap, HashSet, VecDeque}; + + // Build ancestor sets with generation distances for both persons + let mut ancestors1: HashMap<&str, (usize, &Individual)> = HashMap::new(); + let mut queue = VecDeque::new(); + queue.push_back((person1, 0usize)); + + while let Some((current, gen)) = queue.pop_front() { + if let Some(current_xref) = ¤t.xref { + ancestors1.insert(current_xref.as_str(), (gen, current)); + } + + for (father, mother) in self.get_parents(current) { + if let Some(dad) = father { + if let Some(dad_xref) = &dad.xref { + if !ancestors1.contains_key(dad_xref.as_str()) { + queue.push_back((dad, gen + 1)); + } + } + } + if let Some(mom) = mother { + if let Some(mom_xref) = &mom.xref { + if !ancestors1.contains_key(mom_xref.as_str()) { + queue.push_back((mom, gen + 1)); + } + } + } + } + } + + // Find common ancestors and their distances + let mut common_ancestors: Vec<(&Individual, usize, usize)> = Vec::new(); + let mut visited = HashSet::new(); + queue.clear(); + queue.push_back((person2, 0usize)); + + while let Some((current, gen2)) = queue.pop_front() { + if let Some(current_xref) = ¤t.xref { + let xref_str = current_xref.as_str(); + + // Check if this is a common ancestor + if let Some(&(gen1, ancestor)) = ancestors1.get(xref_str) { + common_ancestors.push((ancestor, gen1, gen2)); + } + + if visited.insert(xref_str) { + for (father, mother) in self.get_parents(current) { + if let Some(dad) = father { + queue.push_back((dad, gen2 + 1)); + } + if let Some(mom) = mother { + queue.push_back((mom, gen2 + 1)); + } + } + } + } + } + + if common_ancestors.is_empty() { + return RelationshipResult::none(); + } + + // Find the most recent common ancestor(s) - those with minimum total distance + let min_total_distance = common_ancestors + .iter() + .map(|(_, g1, g2)| g1 + g2) + .min() + .unwrap(); + + let mrca: Vec<&Individual> = common_ancestors + .iter() + .filter(|(_, g1, g2)| g1 + g2 == min_total_distance) + .map(|(ancestor, _, _)| *ancestor) + .collect(); + + // Get the generations for the first MRCA (they should all be the same distance) + let (_, gen1, gen2) = common_ancestors + .iter() + .find(|(_, g1, g2)| g1 + g2 == min_total_distance) + .unwrap(); + + let description = self.describe_relationship(*gen1, *gen2); + + RelationshipResult { + description, + mrca, + generations_to_mrca_1: Some(*gen1), + generations_to_mrca_2: Some(*gen2), + } + } + + /// Generate a human-readable relationship description based on generational distances + fn describe_relationship(&self, generations1: usize, generations2: usize) -> String { + // Direct ancestor/descendant relationships + if generations2 == 0 { + return match generations1 { + 1 => "Parent".to_string(), + 2 => "Grandparent".to_string(), + 3 => "Great-Grandparent".to_string(), + n => format!("{}Great-Grandparent", "Great-".repeat(n - 3)), + }; + } + + if generations1 == 0 { + return match generations2 { + 1 => "Child".to_string(), + 2 => "Grandchild".to_string(), + 3 => "Great-Grandchild".to_string(), + n => format!("{}Great-Grandchild", "Great-".repeat(n - 3)), + }; + } + + // Aunt/Uncle and Niece/Nephew relationships + if generations1 == 1 && generations2 == 2 { + return "Niece/Nephew".to_string(); + } + if generations1 == 2 && generations2 == 1 { + return "Aunt/Uncle".to_string(); + } + + // Grand-Aunt/Grand-Uncle and Grand-Niece/Grand-Nephew + if generations1 == 1 && generations2 >= 3 { + let greats = "Great-".repeat(generations2 - 3); + return format!("{}Grand-Niece/Grand-Nephew", greats); + } + if generations1 >= 3 && generations2 == 1 { + let greats = "Great-".repeat(generations1 - 3); + return format!("{}Grand-Aunt/Grand-Uncle", greats); + } + + // Cousin relationships + let min_gen = generations1.min(generations2); + let max_gen = generations1.max(generations2); + let removed = max_gen - min_gen; + + // Degree of cousinship (1st cousin = 2 generations to MRCA, 2nd = 3, etc.) + let cousin_degree = min_gen.saturating_sub(1); + + if cousin_degree == 0 { + return "Not related".to_string(); + } + + let cousin_ordinal = match cousin_degree { + 1 => "1st".to_string(), + 2 => "2nd".to_string(), + 3 => "3rd".to_string(), + n if n % 10 == 1 && n % 100 != 11 => format!("{}st", n), + n if n % 10 == 2 && n % 100 != 12 => format!("{}nd", n), + n if n % 10 == 3 && n % 100 != 13 => format!("{}rd", n), + n => format!("{}th", n), + }; + + if removed == 0 { + format!("{} Cousin", cousin_ordinal) + } else { + format!("{} Cousin {}x Removed", cousin_ordinal, removed) + } + } +} + +#[cfg(test)] +mod relationship_tests { + use super::*; + + // Helper to create a simple test gedcom structure + fn create_test_gedcom() -> Gedcom { + // This would need actual test data - for now, just demonstrate the structure + Gedcom::default() + } + + #[test] + fn test_relationship_result_none() { + let result = RelationshipResult::none(); + assert_eq!(result.description, "Not related"); + assert!(result.mrca.is_empty()); + assert_eq!(result.generations_to_mrca_1, None); + assert_eq!(result.generations_to_mrca_2, None); + } + + #[test] + fn test_relationship_result_self() { + let result = RelationshipResult::self_relation(); + assert_eq!(result.description, "Self"); + assert!(result.mrca.is_empty()); + assert_eq!(result.generations_to_mrca_1, Some(0)); + assert_eq!(result.generations_to_mrca_2, Some(0)); + } + + #[test] + fn test_describe_relationship_parent() { + let gedcom = create_test_gedcom(); + assert_eq!(gedcom.describe_relationship(1, 0), "Parent"); + assert_eq!(gedcom.describe_relationship(2, 0), "Grandparent"); + assert_eq!(gedcom.describe_relationship(3, 0), "Great-Grandparent"); + assert_eq!( + gedcom.describe_relationship(4, 0), + "Great-Great-Grandparent" + ); + } + + #[test] + fn test_describe_relationship_child() { + let gedcom = create_test_gedcom(); + assert_eq!(gedcom.describe_relationship(0, 1), "Child"); + assert_eq!(gedcom.describe_relationship(0, 2), "Grandchild"); + assert_eq!(gedcom.describe_relationship(0, 3), "Great-Grandchild"); + assert_eq!(gedcom.describe_relationship(0, 4), "Great-Great-Grandchild"); + } + + #[test] + fn test_describe_relationship_aunt_uncle() { + let gedcom = create_test_gedcom(); + assert_eq!(gedcom.describe_relationship(2, 1), "Aunt/Uncle"); + assert_eq!(gedcom.describe_relationship(3, 1), "Grand-Aunt/Grand-Uncle"); + assert_eq!( + gedcom.describe_relationship(4, 1), + "Great-Grand-Aunt/Grand-Uncle" + ); + } + + #[test] + fn test_describe_relationship_niece_nephew() { + let gedcom = create_test_gedcom(); + assert_eq!(gedcom.describe_relationship(1, 2), "Niece/Nephew"); + assert_eq!( + gedcom.describe_relationship(1, 3), + "Grand-Niece/Grand-Nephew" + ); + assert_eq!( + gedcom.describe_relationship(1, 4), + "Great-Grand-Niece/Grand-Nephew" + ); + } + + #[test] + fn test_describe_relationship_cousins() { + let gedcom = create_test_gedcom(); + // First cousins (both 2 generations from common ancestor) + assert_eq!(gedcom.describe_relationship(2, 2), "1st Cousin"); + + // Second cousins (both 3 generations from common ancestor) + assert_eq!(gedcom.describe_relationship(3, 3), "2nd Cousin"); + + // Third cousins + assert_eq!(gedcom.describe_relationship(4, 4), "3rd Cousin"); + } + + #[test] + fn test_describe_relationship_cousins_removed() { + let gedcom = create_test_gedcom(); + // First cousin once removed + assert_eq!(gedcom.describe_relationship(2, 3), "1st Cousin 1x Removed"); + assert_eq!(gedcom.describe_relationship(3, 2), "1st Cousin 1x Removed"); + + // First cousin twice removed + assert_eq!(gedcom.describe_relationship(2, 4), "1st Cousin 2x Removed"); + assert_eq!(gedcom.describe_relationship(4, 2), "1st Cousin 2x Removed"); + + // Second cousin once removed + assert_eq!(gedcom.describe_relationship(3, 4), "2nd Cousin 1x Removed"); + assert_eq!(gedcom.describe_relationship(4, 3), "2nd Cousin 1x Removed"); + + // Second cousin twice removed + assert_eq!(gedcom.describe_relationship(3, 5), "2nd Cousin 2x Removed"); + } + + #[test] + fn test_describe_relationship_higher_order_cousins() { + let gedcom = create_test_gedcom(); + assert_eq!(gedcom.describe_relationship(5, 5), "4th Cousin"); + assert_eq!(gedcom.describe_relationship(6, 6), "5th Cousin"); + assert_eq!(gedcom.describe_relationship(11, 11), "10th Cousin"); + assert_eq!(gedcom.describe_relationship(12, 12), "11th Cousin"); + } +} From fe8a2770aa2286920537cf6068803b7260a15ed8 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sat, 6 Dec 2025 19:05:29 -0500 Subject: [PATCH 2/8] Fix clippy warnings in relationship functions - Collapse nested if statements in find_relationship - Replace unwrap() calls with proper error handling in find_relationship_via_mrca - All clippy warnings resolved with -D warnings - All 271 tests still passing --- src/types/mod.rs | 92 ++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index e2866fb..94e7cdf 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1140,23 +1140,21 @@ impl Gedcom { for (f1, m1) in &parents1 { for (f2, m2) in &parents2 { if let (Some(father1), Some(father2)) = (f1, f2) { - if father1.xref.as_ref() == father2.xref.as_ref() { - if !common_parents + if father1.xref.as_ref() == father2.xref.as_ref() + && !common_parents .iter() .any(|p: &&Individual| p.xref.as_ref() == father1.xref.as_ref()) - { - common_parents.push(*father1); - } + { + common_parents.push(*father1); } } if let (Some(mother1), Some(mother2)) = (m1, m2) { - if mother1.xref.as_ref() == mother2.xref.as_ref() { - if !common_parents + if mother1.xref.as_ref() == mother2.xref.as_ref() + && !common_parents .iter() .any(|p: &&Individual| p.xref.as_ref() == mother1.xref.as_ref()) - { - common_parents.push(*mother1); - } + { + common_parents.push(*mother1); } } } @@ -1249,11 +1247,10 @@ impl Gedcom { } // Find the most recent common ancestor(s) - those with minimum total distance - let min_total_distance = common_ancestors - .iter() - .map(|(_, g1, g2)| g1 + g2) - .min() - .unwrap(); + let min_total_distance = match common_ancestors.iter().map(|(_, g1, g2)| g1 + g2).min() { + Some(min) => min, + None => return RelationshipResult::none(), // No common ancestors + }; let mrca: Vec<&Individual> = common_ancestors .iter() @@ -1262,10 +1259,13 @@ impl Gedcom { .collect(); // Get the generations for the first MRCA (they should all be the same distance) - let (_, gen1, gen2) = common_ancestors + let (_, gen1, gen2) = match common_ancestors .iter() .find(|(_, g1, g2)| g1 + g2 == min_total_distance) - .unwrap(); + { + Some(result) => result, + None => return RelationshipResult::none(), // Should not happen, but be safe + }; let description = self.describe_relationship(*gen1, *gen2); @@ -1277,6 +1277,19 @@ impl Gedcom { } } + /// Helper function to format ordinal numbers (1st, 2nd, 3rd, etc.) + fn format_ordinal(n: usize) -> String { + match n { + 1 => "1st".to_string(), + 2 => "2nd".to_string(), + 3 => "3rd".to_string(), + n if n % 10 == 1 && n % 100 != 11 => format!("{}st", n), + n if n % 10 == 2 && n % 100 != 12 => format!("{}nd", n), + n if n % 10 == 3 && n % 100 != 13 => format!("{}rd", n), + n => format!("{}th", n), + } + } + /// Generate a human-readable relationship description based on generational distances fn describe_relationship(&self, generations1: usize, generations2: usize) -> String { // Direct ancestor/descendant relationships @@ -1285,7 +1298,7 @@ impl Gedcom { 1 => "Parent".to_string(), 2 => "Grandparent".to_string(), 3 => "Great-Grandparent".to_string(), - n => format!("{}Great-Grandparent", "Great-".repeat(n - 3)), + n => format!("{} Great-Grandparent", Self::format_ordinal(n - 3)), }; } @@ -1294,7 +1307,7 @@ impl Gedcom { 1 => "Child".to_string(), 2 => "Grandchild".to_string(), 3 => "Great-Grandchild".to_string(), - n => format!("{}Great-Grandchild", "Great-".repeat(n - 3)), + n => format!("{} Great-Grandchild", Self::format_ordinal(n - 3)), }; } @@ -1308,12 +1321,24 @@ impl Gedcom { // Grand-Aunt/Grand-Uncle and Grand-Niece/Grand-Nephew if generations1 == 1 && generations2 >= 3 { - let greats = "Great-".repeat(generations2 - 3); - return format!("{}Grand-Niece/Grand-Nephew", greats); + return if generations2 == 3 { + "Grand-Niece/Grand-Nephew".to_string() + } else { + format!( + "{} Grand-Niece/Grand-Nephew", + Self::format_ordinal(generations2 - 3) + ) + }; } if generations1 >= 3 && generations2 == 1 { - let greats = "Great-".repeat(generations1 - 3); - return format!("{}Grand-Aunt/Grand-Uncle", greats); + return if generations1 == 3 { + "Grand-Aunt/Grand-Uncle".to_string() + } else { + format!( + "{} Grand-Aunt/Grand-Uncle", + Self::format_ordinal(generations1 - 3) + ) + }; } // Cousin relationships @@ -1328,15 +1353,7 @@ impl Gedcom { return "Not related".to_string(); } - let cousin_ordinal = match cousin_degree { - 1 => "1st".to_string(), - 2 => "2nd".to_string(), - 3 => "3rd".to_string(), - n if n % 10 == 1 && n % 100 != 11 => format!("{}st", n), - n if n % 10 == 2 && n % 100 != 12 => format!("{}nd", n), - n if n % 10 == 3 && n % 100 != 13 => format!("{}rd", n), - n => format!("{}th", n), - }; + let cousin_ordinal = Self::format_ordinal(cousin_degree); if removed == 0 { format!("{} Cousin", cousin_ordinal) @@ -1380,10 +1397,7 @@ mod relationship_tests { assert_eq!(gedcom.describe_relationship(1, 0), "Parent"); assert_eq!(gedcom.describe_relationship(2, 0), "Grandparent"); assert_eq!(gedcom.describe_relationship(3, 0), "Great-Grandparent"); - assert_eq!( - gedcom.describe_relationship(4, 0), - "Great-Great-Grandparent" - ); + assert_eq!(gedcom.describe_relationship(4, 0), "1st Great-Grandparent"); } #[test] @@ -1392,7 +1406,7 @@ mod relationship_tests { assert_eq!(gedcom.describe_relationship(0, 1), "Child"); assert_eq!(gedcom.describe_relationship(0, 2), "Grandchild"); assert_eq!(gedcom.describe_relationship(0, 3), "Great-Grandchild"); - assert_eq!(gedcom.describe_relationship(0, 4), "Great-Great-Grandchild"); + assert_eq!(gedcom.describe_relationship(0, 4), "1st Great-Grandchild"); } #[test] @@ -1402,7 +1416,7 @@ mod relationship_tests { assert_eq!(gedcom.describe_relationship(3, 1), "Grand-Aunt/Grand-Uncle"); assert_eq!( gedcom.describe_relationship(4, 1), - "Great-Grand-Aunt/Grand-Uncle" + "1st Grand-Aunt/Grand-Uncle" ); } @@ -1416,7 +1430,7 @@ mod relationship_tests { ); assert_eq!( gedcom.describe_relationship(1, 4), - "Great-Grand-Niece/Grand-Nephew" + "1st Grand-Niece/Grand-Nephew" ); } From 7d48e34b7ccb50443e9342759b429e37e0c9b631 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sat, 6 Dec 2025 19:24:09 -0500 Subject: [PATCH 3/8] Fix ordinal numbering to start at 2nd for relationship descriptions The base case (Great-Grandparent, Grand-Niece/Nephew) represents the implied '1st' occurrence, so numbering should start at 2nd: - 4 generations: '2nd Great-Grandparent' (was '1st Great-Grandparent') - 5 generations: '3rd Great-Grandparent' (was '2nd Great-Grandparent') - Similar corrections for Great-Grandchild, Grand-Aunt/Uncle, Grand-Niece/Nephew This aligns with the convention that 'Great-Grandparent' implicitly means '1st Great-Grandparent', just as 'Grandparent' means '1st generation up from parent'. --- src/types/mod.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index 94e7cdf..f9da08f 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1298,7 +1298,7 @@ impl Gedcom { 1 => "Parent".to_string(), 2 => "Grandparent".to_string(), 3 => "Great-Grandparent".to_string(), - n => format!("{} Great-Grandparent", Self::format_ordinal(n - 3)), + n => format!("{} Great-Grandparent", Self::format_ordinal(n - 2)), }; } @@ -1307,7 +1307,7 @@ impl Gedcom { 1 => "Child".to_string(), 2 => "Grandchild".to_string(), 3 => "Great-Grandchild".to_string(), - n => format!("{} Great-Grandchild", Self::format_ordinal(n - 3)), + n => format!("{} Great-Grandchild", Self::format_ordinal(n - 2)), }; } @@ -1326,7 +1326,7 @@ impl Gedcom { } else { format!( "{} Grand-Niece/Grand-Nephew", - Self::format_ordinal(generations2 - 3) + Self::format_ordinal(generations2 - 2) ) }; } @@ -1336,7 +1336,7 @@ impl Gedcom { } else { format!( "{} Grand-Aunt/Grand-Uncle", - Self::format_ordinal(generations1 - 3) + Self::format_ordinal(generations1 - 2) ) }; } @@ -1397,7 +1397,7 @@ mod relationship_tests { assert_eq!(gedcom.describe_relationship(1, 0), "Parent"); assert_eq!(gedcom.describe_relationship(2, 0), "Grandparent"); assert_eq!(gedcom.describe_relationship(3, 0), "Great-Grandparent"); - assert_eq!(gedcom.describe_relationship(4, 0), "1st Great-Grandparent"); + assert_eq!(gedcom.describe_relationship(4, 0), "2nd Great-Grandparent"); } #[test] @@ -1406,7 +1406,7 @@ mod relationship_tests { assert_eq!(gedcom.describe_relationship(0, 1), "Child"); assert_eq!(gedcom.describe_relationship(0, 2), "Grandchild"); assert_eq!(gedcom.describe_relationship(0, 3), "Great-Grandchild"); - assert_eq!(gedcom.describe_relationship(0, 4), "1st Great-Grandchild"); + assert_eq!(gedcom.describe_relationship(0, 4), "2nd Great-Grandchild"); } #[test] @@ -1416,7 +1416,7 @@ mod relationship_tests { assert_eq!(gedcom.describe_relationship(3, 1), "Grand-Aunt/Grand-Uncle"); assert_eq!( gedcom.describe_relationship(4, 1), - "1st Grand-Aunt/Grand-Uncle" + "2nd Grand-Aunt/Grand-Uncle" ); } @@ -1430,7 +1430,7 @@ mod relationship_tests { ); assert_eq!( gedcom.describe_relationship(1, 4), - "1st Grand-Niece/Grand-Nephew" + "2nd Grand-Niece/Grand-Nephew" ); } From b4d7622bcc66d6ba9423ce3bc182981df5fd68f5 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sat, 6 Dec 2025 19:28:15 -0500 Subject: [PATCH 4/8] Follow Grandparent pattern for Aunt/Uncle and Niece/Nephew relationships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the same Great-Grand pattern as Grandparents for consistency: Aunt/Uncle progression: - Aunt/Uncle → Grand-Aunt/Grand-Uncle → Great-Grand-Aunt/Grand-Uncle → 2nd Great-Grand-Aunt/Grand-Uncle Niece/Nephew progression: - Niece/Nephew → Grand-Niece/Grand-Nephew → Great-Grand-Niece/Grand-Nephew → 2nd Great-Grand-Niece/Grand-Nephew This matches the Grandparent pattern: - Grandparent → Great-Grandparent → 2nd Great-Grandparent --- src/types/mod.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index f9da08f..cf0cead 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1321,23 +1321,23 @@ impl Gedcom { // Grand-Aunt/Grand-Uncle and Grand-Niece/Grand-Nephew if generations1 == 1 && generations2 >= 3 { - return if generations2 == 3 { - "Grand-Niece/Grand-Nephew".to_string() - } else { - format!( - "{} Grand-Niece/Grand-Nephew", - Self::format_ordinal(generations2 - 2) - ) + return match generations2 { + 3 => "Grand-Niece/Grand-Nephew".to_string(), + 4 => "Great-Grand-Niece/Grand-Nephew".to_string(), + n => format!( + "{} Great-Grand-Niece/Grand-Nephew", + Self::format_ordinal(n - 3) + ), }; } if generations1 >= 3 && generations2 == 1 { - return if generations1 == 3 { - "Grand-Aunt/Grand-Uncle".to_string() - } else { - format!( - "{} Grand-Aunt/Grand-Uncle", - Self::format_ordinal(generations1 - 2) - ) + return match generations1 { + 3 => "Grand-Aunt/Grand-Uncle".to_string(), + 4 => "Great-Grand-Aunt/Grand-Uncle".to_string(), + n => format!( + "{} Great-Grand-Aunt/Grand-Uncle", + Self::format_ordinal(n - 3) + ), }; } @@ -1416,7 +1416,7 @@ mod relationship_tests { assert_eq!(gedcom.describe_relationship(3, 1), "Grand-Aunt/Grand-Uncle"); assert_eq!( gedcom.describe_relationship(4, 1), - "2nd Grand-Aunt/Grand-Uncle" + "Great-Grand-Aunt/Grand-Uncle" ); } @@ -1430,7 +1430,7 @@ mod relationship_tests { ); assert_eq!( gedcom.describe_relationship(1, 4), - "2nd Grand-Niece/Grand-Nephew" + "Great-Grand-Niece/Grand-Nephew" ); } From 1989433e4ed5a7b8d2c2155703c1b35450c98ffd Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sat, 6 Dec 2025 19:29:31 -0500 Subject: [PATCH 5/8] Add roadmap for date parsing --- docs/ROADMAP.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index cbdd00d..92e2a41 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -245,6 +245,14 @@ Filters should allow for the partial parsing of genealogical data. TBD the exten - Implement filtering capabilities for genealogical data - Enhance search and query functionalities +6. **Date Parsing** + +Dates are a freeform field, with some common conventions. We need to implement parsing and validation, as much as it's possible, to enable searching through events by date. + + - Implement date parsing and validation + - Support various date formats and ranges + - Enhance date-related functionalities + ### Medium Priority 4. **Source Record Parsing (SOUR)** ✅ Complete From 77f8a3539af3ae9f5c435929607f28d4efe0e416 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sat, 6 Dec 2025 19:33:56 -0500 Subject: [PATCH 6/8] Address PR review feedback: gender-aware child descriptions and naming fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Check child's gender to return 'Son' or 'Daughter' instead of always using generic 'Child' (fixes both parent checks) - Fix naming inconsistency: 'Great-Grand-Niece/Nephew' → 'Great-Grand-Niece/Great-Grand-Nephew' for consistency with Aunt/Uncle pattern - Update test expectations to match corrected naming --- src/types/mod.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index cf0cead..96f5975 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1109,16 +1109,26 @@ impl Gedcom { // Check if person2 is parent of person1 (inverse) for (father, mother) in self.get_parents(person2) { if father.and_then(|f| f.xref.as_ref()).map(|x| x.as_str()) == Some(xref1) { + let description = match &person2.gender { + individual::Gender::Male => "Son", + individual::Gender::Female => "Daughter", + _ => "Child", + }; return RelationshipResult { - description: "Son".to_string(), + description: description.to_string(), mrca: Vec::new(), generations_to_mrca_1: Some(0), generations_to_mrca_2: Some(1), }; } if mother.and_then(|m| m.xref.as_ref()).map(|x| x.as_str()) == Some(xref1) { + let description = match &person2.gender { + individual::Gender::Male => "Son", + individual::Gender::Female => "Daughter", + _ => "Child", + }; return RelationshipResult { - description: "Daughter".to_string(), + description: description.to_string(), mrca: Vec::new(), generations_to_mrca_1: Some(0), generations_to_mrca_2: Some(1), @@ -1323,9 +1333,9 @@ impl Gedcom { if generations1 == 1 && generations2 >= 3 { return match generations2 { 3 => "Grand-Niece/Grand-Nephew".to_string(), - 4 => "Great-Grand-Niece/Grand-Nephew".to_string(), + 4 => "Great-Grand-Niece/Great-Grand-Nephew".to_string(), n => format!( - "{} Great-Grand-Niece/Grand-Nephew", + "{} Great-Grand-Niece/Great-Grand-Nephew", Self::format_ordinal(n - 3) ), }; @@ -1430,7 +1440,7 @@ mod relationship_tests { ); assert_eq!( gedcom.describe_relationship(1, 4), - "Great-Grand-Niece/Grand-Nephew" + "Great-Grand-Niece/Great-Grand-Nephew" ); } From a8bde5bbe2fec95346c2922133a8419ac69007e7 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sat, 6 Dec 2025 19:37:29 -0500 Subject: [PATCH 7/8] Fix Aunt/Uncle naming consistency for Great-Grand prefix Correct the naming to use 'Great-Grand-Aunt/Great-Grand-Uncle' instead of 'Great-Grand-Aunt/Grand-Uncle' for 4+ generation relationships. This ensures both parts of the relationship name receive the full prefix, matching the pattern used for Niece/Nephew relationships. --- src/types/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index 96f5975..9d1afcc 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1343,9 +1343,9 @@ impl Gedcom { if generations1 >= 3 && generations2 == 1 { return match generations1 { 3 => "Grand-Aunt/Grand-Uncle".to_string(), - 4 => "Great-Grand-Aunt/Grand-Uncle".to_string(), + 4 => "Great-Grand-Aunt/Great-Grand-Uncle".to_string(), n => format!( - "{} Great-Grand-Aunt/Grand-Uncle", + "{} Great-Grand-Aunt/Great-Grand-Uncle", Self::format_ordinal(n - 3) ), }; @@ -1426,7 +1426,7 @@ mod relationship_tests { assert_eq!(gedcom.describe_relationship(3, 1), "Grand-Aunt/Grand-Uncle"); assert_eq!( gedcom.describe_relationship(4, 1), - "Great-Grand-Aunt/Grand-Uncle" + "Great-Grand-Aunt/Great-Grand-Uncle" ); } From 88a75b6f72f457256d0519a7904ebd7f738c7432 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sat, 6 Dec 2025 19:42:13 -0500 Subject: [PATCH 8/8] Refactor Death event matching to use helper function Replace duplicated date matching logic with call to event_date_matches() helper function for consistency with other event types. This improves code maintainability by centralizing the date matching logic. --- src/types/mod.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index 9d1afcc..bd7b8a1 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -268,11 +268,10 @@ impl Gedcom { Self::event_date_matches(&e.event.detail.date, &pattern_lower) }), EventType::Death => individual.death.iter().any(|e| { - e.event - .as_ref() - .and_then(|ev| ev.date.as_ref()) - .map(|d| d.to_lowercase().contains(&pattern_lower)) - .unwrap_or(false) + Self::event_date_matches( + &e.event.as_ref().and_then(|ev| ev.date.clone()), + &pattern_lower, + ) }), EventType::Christening => individual.christening.iter().any(|e| { Self::event_date_matches(&e.event.detail.date, &pattern_lower)