From 9dd19e736d3cc8cd11f3fb83d9000498839902e7 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Fri, 12 Dec 2025 15:20:12 -0500 Subject: [PATCH 1/4] Implement comprehensive CLI with summary feature Replace basic command-line parsing with clap-based robust CLI and add beautiful GEDCOM summary output using tabled. Features added: - --version flag: Display application version - --dump flag: Output entire GEDCOM structure (replaces default behavior) - --verbose flag: Show detailed encoding warnings - --summary flag: Display formatted summary (new default behavior) - --home-xref flag: Specify which individual to use for genealogy analysis Summary output includes: - Source system information from GEDCOM header - GEDCOM version and form - Statistics table with counts of all record types: * Individuals, Families, Sources, Repositories * Notes, Multimedia, Submitters - Validation warnings count (detailed list with --verbose) - Home individual information: * Name, XREF, birth/death dates and places * Genealogy depth (ancestor/descendant generations) * Immediate family counts (parents, siblings, spouses, children) * Extended family counts (total ancestors/descendants up to 10 gens) Implementation details: - Uses clap 4.5 with derive feature for ergonomic CLI - Uses tabled 0.16 for beautiful table formatting - Added calculate_max_generations_ancestors/descendants functions using breadth-first search to find maximum genealogy depth - Leverages existing Gedcom API methods (get_parents, get_children, etc.) - Summary is now default behavior, --dump preserves debug output Resolves TODO in src/main.rs to create pretty summary output. --- Cargo.toml | 2 + src/main.rs | 366 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 303 insertions(+), 65 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e8a1789..eeb080c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,8 @@ panic = "warn" smallvec = "1.10.0" winnow = "0.5.40" encoding_rs = "0.8" +clap = { version = "4.5", features = ["derive"] } +tabled = "0.16" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } diff --git a/src/main.rs b/src/main.rs index 3e31f34..6e3eb84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,63 +1,309 @@ extern crate gedcom_rs; +use clap::Parser; use gedcom_rs::parse::{parse_gedcom, GedcomConfig}; - -use std::env; +use gedcom_rs::types::Gedcom; use std::process; +use tabled::{settings::Style, Table, Tabled}; + +/// GEDCOM 5.5.1 Parser and Analyzer +#[derive(Parser, Debug)] +#[command(name = "gedcom-rs")] +#[command(version)] +#[command(about = "Parse and analyze GEDCOM 5.5.1 genealogical data files", long_about = None)] +struct Args { + /// Path to the GEDCOM file to parse + #[arg(value_name = "FILE")] + filename: String, + + /// Show detailed encoding warnings and diagnostics + #[arg(short, long)] + verbose: bool, + + /// Dump the entire GEDCOM structure (debug output) + #[arg(short, long)] + dump: bool, + + /// Show summary statistics (default behavior) + #[arg(short, long, default_value_t = true)] + summary: bool, + + /// XREF of the individual to use as the "home" person for genealogy analysis + #[arg(long, value_name = "XREF")] + home_xref: Option, +} + +/// Statistics row for the summary table +#[derive(Tabled)] +struct StatRow { + #[tabled(rename = "Record Type")] + record_type: String, + #[tabled(rename = "Count")] + count: usize, +} fn main() { - let args: Vec = env::args().collect(); - - // Parse flags and filename - let mut config = GedcomConfig::new(); - let mut filename: Option<&String> = None; - - for arg in args.iter().skip(1) { - match arg.as_str() { - "--help" | "-h" => usage(""), - "--verbose" | "-v" => config.verbose = true, - _ => { - if filename.is_some() { - usage(&format!("Unexpected argument: {}", arg)); - } - filename = Some(arg); - } - } - } + let args = Args::parse(); - let filename = match filename { - Some(f) => f, - None => { - usage("Missing filename."); - unreachable!() - } + // Configure parser + let config = if args.verbose { + GedcomConfig::new().verbose() + } else { + GedcomConfig::new() }; - match parse_gedcom(filename, &config) { - Ok(gedcom) => { - // TODO: print a pretty summary of the gedcom. Use `tabled` crate? - println!("{:#?}", gedcom); - } + // Parse the GEDCOM file + let gedcom = match parse_gedcom(&args.filename, &config) { + Ok(g) => g, Err(e) => { eprintln!("Error parsing GEDCOM file: {}", e); process::exit(1); } + }; + + // Handle --dump flag + if args.dump { + println!("{:#?}", gedcom); + return; } -} -fn usage(msg: &str) { - if !msg.is_empty() { - println!("{msg}"); + // Default behavior: show summary + if args.summary { + print_summary(&gedcom, args.home_xref.as_deref(), args.verbose); } - println!("Usage: gedcom-test [OPTIONS] "); +} + +fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { + println!("═══════════════════════════════════════════════════════════"); + println!(" GEDCOM FILE SUMMARY"); + println!("═══════════════════════════════════════════════════════════"); println!(); - println!("Arguments:"); - println!(" Path to the GEDCOM file to parse"); + + // Display file name/source from header + if let Some(ref source) = gedcom.header.source { + println!("Source System: {}", source.source); + if let Some(ref name) = source.name { + println!("Source Name: {}", name); + } + if let Some(ref version) = source.version { + println!("Version: {}", version); + } + println!(); + } + + // Display GEDCOM version + if let Some(ref gedc) = gedcom.header.gedcom_version { + if let Some(ref version) = gedc.version { + println!("GEDCOM Version: {}", version); + } + if let Some(ref form) = gedc.form { + println!("GEDCOM Form: {:?}", form); + } + println!(); + } + + // Build statistics table + let stats = vec![ + StatRow { + record_type: "Individuals".to_string(), + count: gedcom.individuals.len(), + }, + StatRow { + record_type: "Families".to_string(), + count: gedcom.families.len(), + }, + StatRow { + record_type: "Sources".to_string(), + count: gedcom.sources.len(), + }, + StatRow { + record_type: "Repositories".to_string(), + count: gedcom.repositories.len(), + }, + StatRow { + record_type: "Notes".to_string(), + count: gedcom.notes.len(), + }, + StatRow { + record_type: "Multimedia".to_string(), + count: gedcom.multimedia.len(), + }, + StatRow { + record_type: "Submitters".to_string(), + count: gedcom.submitters.len(), + }, + ]; + + let mut table = Table::new(stats); + table.with(Style::modern()); + println!("{}", table); println!(); - println!("Options:"); - println!(" -v, --verbose Show detailed encoding warnings and diagnostics"); - println!(" -h, --help Show this help message"); - std::process::exit(0x0100); + + // Display warnings if any + if gedcom.has_warnings() { + println!("⚠ Warnings: {}", gedcom.warnings.len()); + if verbose { + println!(); + println!("Warning Details:"); + for warning in &gedcom.warnings { + println!(" • {}", warning); + } + } + println!(); + } + + // Home individual analysis + let home_individual = if let Some(xref) = home_xref { + gedcom.find_individual_by_xref(xref) + } else { + gedcom.individuals.first() + }; + + if let Some(individual) = home_individual { + println!("───────────────────────────────────────────────────────────"); + println!(" HOME INDIVIDUAL"); + println!("───────────────────────────────────────────────────────────"); + println!(); + + // Display name + if let Some(name) = individual.names.first() { + if let Some(ref name_value) = name.name.value { + println!("Name: {}", name_value); + } + if let Some(ref given) = name.name.given { + println!("Given Name: {}", given); + } + if let Some(ref surname) = name.name.surname { + println!("Surname: {}", surname); + } + } + + // Display XREF + if let Some(ref xref) = individual.xref { + println!("XREF: {}", xref.as_str()); + } + + // Display birth info + if let Some(birth) = individual.birth.first() { + if let Some(ref date) = birth.event.detail.date { + println!("Birth Date: {}", date); + } + if let Some(ref place) = birth.event.detail.place { + if let Some(ref place_name) = place.name { + println!("Birth Place: {}", place_name); + } + } + } + + // Display death info + if let Some(death) = individual.death.first() { + if let Some(ref event) = death.event { + if let Some(ref date) = event.date { + println!("Death Date: {}", date); + } + if let Some(ref place) = event.place { + if let Some(ref place_name) = place.name { + println!("Death Place: {}", place_name); + } + } + } + } + + println!(); + + // Calculate genealogy statistics + let max_ancestor_gens = calculate_max_generations_ancestors(gedcom, individual); + let max_descendant_gens = calculate_max_generations_descendants(gedcom, individual); + + println!("Genealogy Depth:"); + println!(" Ancestor Generations: {}", max_ancestor_gens); + println!(" Descendant Generations: {}", max_descendant_gens); + println!( + " Total Generations: {}", + max_ancestor_gens + max_descendant_gens + ); + println!(); + + // Display immediate family counts + let parents = gedcom.get_parents(individual); + let children = gedcom.get_children(individual); + let siblings = gedcom.get_siblings(individual); + let spouses = gedcom.get_spouses(individual); + + println!("Immediate Family:"); + println!(" Parents: {}", if parents.is_empty() { 0 } else { 2 }); + println!(" Siblings: {}", siblings.len()); + println!(" Spouses: {}", spouses.len()); + println!(" Children: {}", children.len()); + println!(); + + // Extended family counts + let ancestors = gedcom.get_ancestors(individual, Some(10)); + let descendants = gedcom.get_descendants(individual, Some(10)); + + println!("Extended Family (up to 10 generations):"); + println!(" Total Ancestors: {}", ancestors.len()); + println!(" Total Descendants: {}", descendants.len()); + println!(); + } else { + println!("No individuals found in GEDCOM file."); + println!(); + } + + println!("═══════════════════════════════════════════════════════════"); +} + +/// Calculate maximum number of ancestor generations from the given individual +fn calculate_max_generations_ancestors( + gedcom: &Gedcom, + individual: &gedcom_rs::types::Individual, +) -> usize { + use std::collections::VecDeque; + + let mut max_depth = 0; + let mut queue = VecDeque::new(); + queue.push_back((individual, 0)); + + while let Some((current, depth)) = queue.pop_front() { + if depth > max_depth { + max_depth = depth; + } + + for (father, mother) in gedcom.get_parents(current) { + if let Some(dad) = father { + queue.push_back((dad, depth + 1)); + } + if let Some(mom) = mother { + queue.push_back((mom, depth + 1)); + } + } + } + + max_depth +} + +/// Calculate maximum number of descendant generations from the given individual +fn calculate_max_generations_descendants( + gedcom: &Gedcom, + individual: &gedcom_rs::types::Individual, +) -> usize { + use std::collections::VecDeque; + + let mut max_depth = 0; + let mut queue = VecDeque::new(); + queue.push_back((individual, 0)); + + while let Some((current, depth)) = queue.pop_front() { + if depth > max_depth { + max_depth = depth; + } + + for child in gedcom.get_children(current) { + queue.push_back((child, depth + 1)); + } + } + + max_depth } #[allow(clippy::unwrap_used)] @@ -71,8 +317,6 @@ mod tests { let gedcom = parse_gedcom("./data/complete.ged", &GedcomConfig::new()).unwrap(); // Test the header - // println!("Gedcom: {:?}", gedcom.header); - // Test the copyright header assert!(gedcom.header.copyright.is_some()); let copyright = gedcom.header.copyright.unwrap(); assert!( @@ -289,27 +533,19 @@ mod tests { subm.change_date.as_ref().unwrap().date, Some("7 SEP 2000".to_string()) ); - - // Check automated record ID - // (Duplicate assertion removed) } - // #[test] - // /// Tests a possible bug in Ancestry's format, if a line break is embedded within the content of a note - // /// As far as I can tell, it's a \n embedded into the note, at least, from a hex dump of that content. - // fn newline_in_note() { - // let data = vec![ - // "0 @S313871942@ SOUR", - // "1 TITL Germany, Lutheran Baptisms, Marriages, and Burials, 1567-1945", - // "1 AUTH Ancestry.com", - // "1 PUBL Ancestry.com Operations, Inc.", - // "1 NOTE

Mikrofilm Sammlung. Familysearch.org

", - // "

Originale: Lutherische Kirchenbücher, 1567-1945. Various sources.

", - // "1 _APID 1,61250::0", - // ]; - - // // assert_eq!(expected, line("\r")("0 HEAD\r").unwrap()); - // // assert_eq!(expected, line("\n")("0 HEAD\n").unwrap()); - // // assert_eq!(expected, line("\r\n")("0 HEAD\r\n").unwrap()); - // } + #[test] + fn test_max_generations_calculation() { + let gedcom = parse_gedcom("./data/complete.ged", &GedcomConfig::new()).unwrap(); + + if let Some(individual) = gedcom.individuals.first() { + let ancestor_gens = calculate_max_generations_ancestors(&gedcom, individual); + let descendant_gens = calculate_max_generations_descendants(&gedcom, individual); + + // Just verify the functions run without panicking + assert!(ancestor_gens >= 0); + assert!(descendant_gens >= 0); + } + } } From 1eee793c7bd210aa01faf63bcb0a7e0c772c960f Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Fri, 12 Dec 2025 17:08:51 -0500 Subject: [PATCH 2/4] Add responsive two-column layout and fix test warnings - Add term_size dependency for terminal width detection - Implement responsive layout: wide (>=120 cols) vs narrow (<120 cols) - Wide layout displays file summary and home individual side-by-side - Narrow layout displays them sequentially (existing behavior) - Refactor print_summary into modular helper functions for maintainability - Fix unused import warning in src/types/mod.rs (removed std::fs) - Fix useless comparison warnings in src/main.rs test (usize >= 0) - All 277 tests pass, clippy clean, properly formatted --- Cargo.toml | 1 + src/main.rs | 251 +++++++++++++++++++++++++++++++++++++++++------ src/types/mod.rs | 1 - 3 files changed, 224 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eeb080c..0d66120 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ winnow = "0.5.40" encoding_rs = "0.8" clap = { version = "4.5", features = ["derive"] } tabled = "0.16" +term_size = "0.3" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } diff --git a/src/main.rs b/src/main.rs index 6e3eb84..728c14a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,12 +74,69 @@ fn main() { } fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { + // Get terminal width, default to 80 if unable to detect + let term_width = term_size::dimensions().map(|(w, _)| w).unwrap_or(80); + + // Use side-by-side layout if terminal is wide enough (>= 120 columns) + if term_width >= 120 { + print_summary_wide(gedcom, home_xref, verbose); + } else { + print_summary_narrow(gedcom, home_xref, verbose); + } +} + +/// Print summary in narrow (single column) format +fn print_summary_narrow(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { println!("═══════════════════════════════════════════════════════════"); println!(" GEDCOM FILE SUMMARY"); println!("═══════════════════════════════════════════════════════════"); println!(); - // Display file name/source from header + print_file_info(gedcom); + print_statistics_table(gedcom); + print_warnings(gedcom, verbose); + + println!("───────────────────────────────────────────────────────────"); + println!(" HOME INDIVIDUAL"); + println!("───────────────────────────────────────────────────────────"); + println!(); + + print_home_individual(gedcom, home_xref); + println!("═══════════════════════════════════════════════════════════"); +} + +/// Print summary in wide (two column) format +fn print_summary_wide(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { + println!("═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════"); + println!(" GEDCOM FILE SUMMARY │ HOME INDIVIDUAL"); + println!("═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════"); + println!(); + + // Collect left column content + let mut left_lines = Vec::new(); + collect_file_info(gedcom, &mut left_lines); + collect_statistics(gedcom, &mut left_lines); + collect_warnings(gedcom, verbose, &mut left_lines); + + // Collect right column content + let mut right_lines = Vec::new(); + collect_home_individual(gedcom, home_xref, &mut right_lines); + + // Print side by side + let max_lines = left_lines.len().max(right_lines.len()); + for i in 0..max_lines { + let left = left_lines.get(i).map(|s| s.as_str()).unwrap_or(""); + let right = right_lines.get(i).map(|s| s.as_str()).unwrap_or(""); + + // Left column is 58 chars wide, right column starts at position 60 + println!("{:<58} │ {}", left, right); + } + + println!("═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════"); +} + +/// Print file information (narrow mode) +fn print_file_info(gedcom: &Gedcom) { if let Some(ref source) = gedcom.header.source { println!("Source System: {}", source.source); if let Some(ref name) = source.name { @@ -91,7 +148,6 @@ fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { println!(); } - // Display GEDCOM version if let Some(ref gedc) = gedcom.header.gedcom_version { if let Some(ref version) = gedc.version { println!("GEDCOM Version: {}", version); @@ -101,9 +157,57 @@ fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { } println!(); } +} + +/// Collect file information into lines (wide mode) +fn collect_file_info(gedcom: &Gedcom, lines: &mut Vec) { + if let Some(ref source) = gedcom.header.source { + lines.push(format!("Source System: {}", source.source)); + if let Some(ref name) = source.name { + lines.push(format!("Source Name: {}", name)); + } + if let Some(ref version) = source.version { + lines.push(format!("Version: {}", version)); + } + lines.push(String::new()); + } + + if let Some(ref gedc) = gedcom.header.gedcom_version { + if let Some(ref version) = gedc.version { + lines.push(format!("GEDCOM Version: {}", version)); + } + if let Some(ref form) = gedc.form { + lines.push(format!("GEDCOM Form: {:?}", form)); + } + lines.push(String::new()); + } +} + +/// Print statistics table (narrow mode) +fn print_statistics_table(gedcom: &Gedcom) { + let stats = build_stats_vec(gedcom); + let mut table = Table::new(stats); + table.with(Style::modern()); + println!("{}", table); + println!(); +} + +/// Collect statistics into lines (wide mode) +fn collect_statistics(gedcom: &Gedcom, lines: &mut Vec) { + let stats = build_stats_vec(gedcom); + let mut table = Table::new(stats); + table.with(Style::modern()); - // Build statistics table - let stats = vec![ + // Split table into lines + for line in table.to_string().lines() { + lines.push(line.to_string()); + } + lines.push(String::new()); +} + +/// Build statistics vector +fn build_stats_vec(gedcom: &Gedcom) -> Vec { + vec![ StatRow { record_type: "Individuals".to_string(), count: gedcom.individuals.len(), @@ -132,14 +236,11 @@ fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { record_type: "Submitters".to_string(), count: gedcom.submitters.len(), }, - ]; - - let mut table = Table::new(stats); - table.with(Style::modern()); - println!("{}", table); - println!(); + ] +} - // Display warnings if any +/// Print warnings (narrow mode) +fn print_warnings(gedcom: &Gedcom, verbose: bool) { if gedcom.has_warnings() { println!("⚠ Warnings: {}", gedcom.warnings.len()); if verbose { @@ -151,8 +252,25 @@ fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { } println!(); } +} - // Home individual analysis +/// Collect warnings into lines (wide mode) +fn collect_warnings(gedcom: &Gedcom, verbose: bool, lines: &mut Vec) { + if gedcom.has_warnings() { + lines.push(format!("⚠ Warnings: {}", gedcom.warnings.len())); + if verbose { + lines.push(String::new()); + lines.push("Warning Details:".to_string()); + for warning in &gedcom.warnings { + lines.push(format!(" • {}", warning)); + } + } + lines.push(String::new()); + } +} + +/// Print home individual information (narrow mode) +fn print_home_individual(gedcom: &Gedcom, home_xref: Option<&str>) { let home_individual = if let Some(xref) = home_xref { gedcom.find_individual_by_xref(xref) } else { @@ -160,11 +278,6 @@ fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { }; if let Some(individual) = home_individual { - println!("───────────────────────────────────────────────────────────"); - println!(" HOME INDIVIDUAL"); - println!("───────────────────────────────────────────────────────────"); - println!(); - // Display name if let Some(name) = individual.names.first() { if let Some(ref name_value) = name.name.value { @@ -178,12 +291,10 @@ fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { } } - // Display XREF if let Some(ref xref) = individual.xref { println!("XREF: {}", xref.as_str()); } - // Display birth info if let Some(birth) = individual.birth.first() { if let Some(ref date) = birth.event.detail.date { println!("Birth Date: {}", date); @@ -195,7 +306,6 @@ fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { } } - // Display death info if let Some(death) = individual.death.first() { if let Some(ref event) = death.event { if let Some(ref date) = event.date { @@ -211,7 +321,6 @@ fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { println!(); - // Calculate genealogy statistics let max_ancestor_gens = calculate_max_generations_ancestors(gedcom, individual); let max_descendant_gens = calculate_max_generations_descendants(gedcom, individual); @@ -224,7 +333,6 @@ fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { ); println!(); - // Display immediate family counts let parents = gedcom.get_parents(individual); let children = gedcom.get_children(individual); let siblings = gedcom.get_siblings(individual); @@ -237,7 +345,6 @@ fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { println!(" Children: {}", children.len()); println!(); - // Extended family counts let ancestors = gedcom.get_ancestors(individual, Some(10)); let descendants = gedcom.get_descendants(individual, Some(10)); @@ -249,8 +356,98 @@ fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { println!("No individuals found in GEDCOM file."); println!(); } +} - println!("═══════════════════════════════════════════════════════════"); +/// Collect home individual information into lines (wide mode) +fn collect_home_individual(gedcom: &Gedcom, home_xref: Option<&str>, lines: &mut Vec) { + let home_individual = if let Some(xref) = home_xref { + gedcom.find_individual_by_xref(xref) + } else { + gedcom.individuals.first() + }; + + if let Some(individual) = home_individual { + if let Some(name) = individual.names.first() { + if let Some(ref name_value) = name.name.value { + lines.push(format!("Name: {}", name_value)); + } + if let Some(ref given) = name.name.given { + lines.push(format!("Given Name: {}", given)); + } + if let Some(ref surname) = name.name.surname { + lines.push(format!("Surname: {}", surname)); + } + } + + if let Some(ref xref) = individual.xref { + lines.push(format!("XREF: {}", xref.as_str())); + } + + if let Some(birth) = individual.birth.first() { + if let Some(ref date) = birth.event.detail.date { + lines.push(format!("Birth Date: {}", date)); + } + if let Some(ref place) = birth.event.detail.place { + if let Some(ref place_name) = place.name { + lines.push(format!("Birth Place: {}", place_name)); + } + } + } + + if let Some(death) = individual.death.first() { + if let Some(ref event) = death.event { + if let Some(ref date) = event.date { + lines.push(format!("Death Date: {}", date)); + } + if let Some(ref place) = event.place { + if let Some(ref place_name) = place.name { + lines.push(format!("Death Place: {}", place_name)); + } + } + } + } + + lines.push(String::new()); + + let max_ancestor_gens = calculate_max_generations_ancestors(gedcom, individual); + let max_descendant_gens = calculate_max_generations_descendants(gedcom, individual); + + lines.push("Genealogy Depth:".to_string()); + lines.push(format!(" Ancestors: {} generations", max_ancestor_gens)); + lines.push(format!( + " Descendants: {} generations", + max_descendant_gens + )); + lines.push(format!( + " Total: {} generations", + max_ancestor_gens + max_descendant_gens + )); + lines.push(String::new()); + + let parents = gedcom.get_parents(individual); + let children = gedcom.get_children(individual); + let siblings = gedcom.get_siblings(individual); + let spouses = gedcom.get_spouses(individual); + + lines.push("Immediate Family:".to_string()); + lines.push(format!( + " Parents: {}", + if parents.is_empty() { 0 } else { 2 } + )); + lines.push(format!(" Siblings: {}", siblings.len())); + lines.push(format!(" Spouses: {}", spouses.len())); + lines.push(format!(" Children: {}", children.len())); + lines.push(String::new()); + + let ancestors = gedcom.get_ancestors(individual, Some(10)); + let descendants = gedcom.get_descendants(individual, Some(10)); + + lines.push("Extended Family:".to_string()); + lines.push(format!(" Ancestors: {}", ancestors.len())); + lines.push(format!(" Descendants: {}", descendants.len())); + } else { + lines.push("No individuals found.".to_string()); + } } /// Calculate maximum number of ancestor generations from the given individual @@ -540,12 +737,10 @@ mod tests { let gedcom = parse_gedcom("./data/complete.ged", &GedcomConfig::new()).unwrap(); if let Some(individual) = gedcom.individuals.first() { - let ancestor_gens = calculate_max_generations_ancestors(&gedcom, individual); - let descendant_gens = calculate_max_generations_descendants(&gedcom, individual); + let _ancestor_gens = calculate_max_generations_ancestors(&gedcom, individual); + let _descendant_gens = calculate_max_generations_descendants(&gedcom, individual); // Just verify the functions run without panicking - assert!(ancestor_gens >= 0); - assert!(descendant_gens >= 0); } } } diff --git a/src/types/mod.rs b/src/types/mod.rs index d95d360..73916ca 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1517,7 +1517,6 @@ mod relationship_tests { #[cfg(test)] mod api_tests { use super::*; - use std::fs; /// Helper to create a test GEDCOM with family data for API testing /// Returns a Gedcom with 6 individuals and 2 families From 103e7a7df3687da4e95ead6ec39473d97f99e109 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Fri, 12 Dec 2025 17:15:41 -0500 Subject: [PATCH 3/4] Display only GEDCOM Form name instead of full debug format - Change GEDCOM Form display from Debug format to just the name field - Before: 'GEDCOM Form: Form { name: Some("LINEAGE-LINKED"), version: Some("5.5.5") }' - After: 'GEDCOM Form: LINEAGE-LINKED' - Updated both print_file_info() and collect_file_info() functions --- src/main.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 728c14a..9c6a9ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -153,7 +153,9 @@ fn print_file_info(gedcom: &Gedcom) { println!("GEDCOM Version: {}", version); } if let Some(ref form) = gedc.form { - println!("GEDCOM Form: {:?}", form); + if let Some(ref name) = form.name { + println!("GEDCOM Form: {}", name); + } } println!(); } @@ -177,7 +179,9 @@ fn collect_file_info(gedcom: &Gedcom, lines: &mut Vec) { lines.push(format!("GEDCOM Version: {}", version)); } if let Some(ref form) = gedc.form { - lines.push(format!("GEDCOM Form: {:?}", form)); + if let Some(ref name) = form.name { + lines.push(format!("GEDCOM Form: {}", name)); + } } lines.push(String::new()); } From 44adeece7e10be5a93289515572ed1dceba771aa Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Fri, 12 Dec 2025 17:32:49 -0500 Subject: [PATCH 4/4] Address PR feedback: improve CLI and fix parent counting Feedback from PR #18 (Copilot review): 1. Remove --summary flag (confusing with --dump) - Summary is now the default behavior - Use --dump for debug output - Simplifies CLI interface 2. Fix parent count logic - Previously assumed 2 parents if any parents existed - Now correctly counts non-None parents from get_parents() - Handles cases like (Some(father), None) properly - Applied fix to both narrow and wide layout functions 3. Define terminal width constants - DEFAULT_TERMINAL_WIDTH = 80 - WIDE_LAYOUT_THRESHOLD = 120 - Improves code clarity and maintainability All tests pass, clippy clean. --- src/main.rs | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9c6a9ee..8248951 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,10 @@ use gedcom_rs::types::Gedcom; use std::process; use tabled::{settings::Style, Table, Tabled}; +// Constants for terminal layout +const DEFAULT_TERMINAL_WIDTH: usize = 80; +const WIDE_LAYOUT_THRESHOLD: usize = 120; + /// GEDCOM 5.5.1 Parser and Analyzer #[derive(Parser, Debug)] #[command(name = "gedcom-rs")] @@ -24,10 +28,6 @@ struct Args { #[arg(short, long)] dump: bool, - /// Show summary statistics (default behavior) - #[arg(short, long, default_value_t = true)] - summary: bool, - /// XREF of the individual to use as the "home" person for genealogy analysis #[arg(long, value_name = "XREF")] home_xref: Option, @@ -61,24 +61,23 @@ fn main() { } }; - // Handle --dump flag + // Handle --dump flag (debug output) if args.dump { println!("{:#?}", gedcom); - return; - } - - // Default behavior: show summary - if args.summary { + } else { + // Default behavior: show summary print_summary(&gedcom, args.home_xref.as_deref(), args.verbose); } } fn print_summary(gedcom: &Gedcom, home_xref: Option<&str>, verbose: bool) { - // Get terminal width, default to 80 if unable to detect - let term_width = term_size::dimensions().map(|(w, _)| w).unwrap_or(80); + // Get terminal width, default to DEFAULT_TERMINAL_WIDTH if unable to detect + let term_width = term_size::dimensions() + .map(|(w, _)| w) + .unwrap_or(DEFAULT_TERMINAL_WIDTH); - // Use side-by-side layout if terminal is wide enough (>= 120 columns) - if term_width >= 120 { + // Use side-by-side layout if terminal is wide enough + if term_width >= WIDE_LAYOUT_THRESHOLD { print_summary_wide(gedcom, home_xref, verbose); } else { print_summary_narrow(gedcom, home_xref, verbose); @@ -342,8 +341,14 @@ fn print_home_individual(gedcom: &Gedcom, home_xref: Option<&str>) { let siblings = gedcom.get_siblings(individual); let spouses = gedcom.get_spouses(individual); + // Count actual non-None parents + let parent_count = parents + .iter() + .map(|(f, m)| f.is_some() as usize + m.is_some() as usize) + .sum::(); + println!("Immediate Family:"); - println!(" Parents: {}", if parents.is_empty() { 0 } else { 2 }); + println!(" Parents: {}", parent_count); println!(" Siblings: {}", siblings.len()); println!(" Spouses: {}", spouses.len()); println!(" Children: {}", children.len()); @@ -433,11 +438,14 @@ fn collect_home_individual(gedcom: &Gedcom, home_xref: Option<&str>, lines: &mut let siblings = gedcom.get_siblings(individual); let spouses = gedcom.get_spouses(individual); + // Count actual non-None parents + let parent_count = parents + .iter() + .map(|(f, m)| f.is_some() as usize + m.is_some() as usize) + .sum::(); + lines.push("Immediate Family:".to_string()); - lines.push(format!( - " Parents: {}", - if parents.is_empty() { 0 } else { 2 } - )); + lines.push(format!(" Parents: {}", parent_count)); lines.push(format!(" Siblings: {}", siblings.len())); lines.push(format!(" Spouses: {}", spouses.len())); lines.push(format!(" Children: {}", children.len()));