Skip to main content

ged_io/
display.rs

1//! Display trait implementations for GEDCOM data structures.
2//!
3//! This module provides human-readable string representations for core GEDCOM types,
4//! making it easier to print and display genealogical data.
5
6use std::fmt;
7
8use crate::types::{
9    family::Family,
10    header::Header,
11    individual::{name::Name, Individual},
12    multimedia::Multimedia,
13    note::Note,
14    repository::Repository,
15    source::Source,
16    submission::Submission,
17    submitter::Submitter,
18    GedcomData,
19};
20
21impl fmt::Display for GedcomData {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        writeln!(f, "GEDCOM Data")?;
24        writeln!(f, "============")?;
25
26        if let Some(ref header) = self.header {
27            writeln!(f, "{header}")?;
28        }
29
30        if !self.individuals.is_empty() {
31            writeln!(f, "\nIndividuals ({}):", self.individuals.len())?;
32            for individual in &self.individuals {
33                writeln!(f, "  {individual}")?;
34            }
35        }
36
37        if !self.families.is_empty() {
38            writeln!(f, "\nFamilies ({}):", self.families.len())?;
39            for family in &self.families {
40                writeln!(f, "  {family}")?;
41            }
42        }
43
44        if !self.sources.is_empty() {
45            writeln!(f, "\nSources ({}):", self.sources.len())?;
46            for source in &self.sources {
47                writeln!(f, "  {source}")?;
48            }
49        }
50
51        if !self.repositories.is_empty() {
52            writeln!(f, "\nRepositories ({}):", self.repositories.len())?;
53            for repo in &self.repositories {
54                writeln!(f, "  {repo}")?;
55            }
56        }
57
58        if !self.multimedia.is_empty() {
59            writeln!(f, "\nMultimedia ({}):", self.multimedia.len())?;
60            for media in &self.multimedia {
61                writeln!(f, "  {media}")?;
62            }
63        }
64
65        if !self.submitters.is_empty() {
66            writeln!(f, "\nSubmitters ({}):", self.submitters.len())?;
67            for submitter in &self.submitters {
68                writeln!(f, "  {submitter}")?;
69            }
70        }
71
72        if !self.submissions.is_empty() {
73            writeln!(f, "\nSubmissions ({}):", self.submissions.len())?;
74            for submission in &self.submissions {
75                writeln!(f, "  {submission}")?;
76            }
77        }
78
79        Ok(())
80    }
81}
82
83impl fmt::Display for Header {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        write!(f, "Header")?;
86
87        if let Some(ref gedcom) = self.gedcom {
88            if let Some(ref version) = gedcom.version {
89                write!(f, " (GEDCOM {version})")?;
90            }
91        }
92
93        if let Some(ref source) = self.source {
94            if let Some(ref name) = source.name {
95                write!(f, " - Source: {name}")?;
96            }
97        }
98
99        if let Some(ref encoding) = self.encoding {
100            if let Some(ref value) = encoding.value {
101                write!(f, " [{value}]")?;
102            }
103        }
104
105        Ok(())
106    }
107}
108
109impl fmt::Display for Individual {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        // Start with xref if available
112        if let Some(ref xref) = self.xref {
113            write!(f, "{xref} ")?;
114        }
115
116        // Display name
117        if let Some(ref name) = self.name {
118            write!(f, "{name}")?;
119        } else {
120            write!(f, "(Unknown Name)")?;
121        }
122
123        // Display sex if available
124        if let Some(ref sex) = self.sex {
125            write!(f, " ({})", sex.value)?;
126        }
127
128        let mut birth_date: Option<&str> = None;
129        let mut baptism_date: Option<&str> = None;
130        let mut death_date: Option<&str> = None;
131        let mut inhumation_date: Option<&str> = None;
132
133        for event in &self.events {
134            match event.event {
135                crate::types::event::Event::Birth if birth_date.is_none() => {
136                    birth_date = event.date.as_ref().and_then(|d| d.value.as_deref());
137                }
138                crate::types::event::Event::Baptism if baptism_date.is_none() => {
139                    baptism_date = event.date.as_ref().and_then(|d| d.value.as_deref());
140                }
141                crate::types::event::Event::Death if death_date.is_none() => {
142                    death_date = event.date.as_ref().and_then(|d| d.value.as_deref());
143                }
144                crate::types::event::Event::Burial if inhumation_date.is_none() => {
145                    inhumation_date = event.date.as_ref().and_then(|d| d.value.as_deref());
146                }
147                _ => {}
148            }
149        }
150
151        if let Some(date) = birth_date.or(baptism_date) {
152            write!(f, ", b. {date}")?;
153        }
154
155        if let Some(date) = death_date.or(inhumation_date) {
156            write!(f, ", d. {date}")?;
157        }
158
159        Ok(())
160    }
161}
162
163impl fmt::Display for Name {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        if let Some(ref value) = self.value {
166            // GEDCOM names use slashes around surnames, e.g., "John /Doe/"
167            // We display them more naturally
168            let display_name = value.replace('/', "").trim().to_string();
169            if display_name.is_empty() {
170                write!(f, "(Unknown)")?;
171            } else {
172                write!(f, "{display_name}")?;
173            }
174        } else {
175            // Build from components if no full value
176            let mut parts = Vec::new();
177
178            if let Some(ref prefix) = self.prefix {
179                parts.push(prefix.clone());
180            }
181            if let Some(ref given) = self.given {
182                parts.push(given.clone());
183            }
184            if let Some(ref surname_prefix) = self.surname_prefix {
185                parts.push(surname_prefix.clone());
186            }
187            if let Some(ref surname) = self.surname {
188                parts.push(surname.clone());
189            }
190            if let Some(ref suffix) = self.suffix {
191                parts.push(suffix.clone());
192            }
193
194            if parts.is_empty() {
195                write!(f, "(Unknown)")?;
196            } else {
197                write!(f, "{}", parts.join(" "))?;
198            }
199        }
200
201        Ok(())
202    }
203}
204
205impl fmt::Display for Family {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        if let Some(ref xref) = self.xref {
208            write!(f, "{xref} ")?;
209        }
210
211        let mut members = Vec::new();
212
213        if let Some(ref ind1) = self.individual1 {
214            members.push(format!("Partner 1: {ind1}"));
215        }
216        if let Some(ref ind2) = self.individual2 {
217            members.push(format!("Partner 2: {ind2}"));
218        }
219
220        if members.is_empty() {
221            write!(f, "(No partners)")?;
222        } else {
223            write!(f, "{}", members.join(", "))?;
224        }
225
226        if !self.children.is_empty() {
227            write!(f, " [{} child(ren)]", self.children.len())?;
228        }
229
230        let mut marriage_date: Option<&str> = None;
231        let mut engagement_date: Option<&str> = None;
232        let mut separated_date: Option<&str> = None;
233        let mut divorce_date: Option<&str> = None;
234        let mut annulment_date: Option<&str> = None;
235
236        for event in &self.events {
237            match event.event {
238                crate::types::event::Event::Marriage if marriage_date.is_none() => {
239                    marriage_date = event.date.as_ref().and_then(|d| d.value.as_deref());
240                }
241                crate::types::event::Event::Engagement if engagement_date.is_none() => {
242                    engagement_date = event.date.as_ref().and_then(|d| d.value.as_deref());
243                }
244                crate::types::event::Event::Separated if separated_date.is_none() => {
245                    separated_date = event.date.as_ref().and_then(|d| d.value.as_deref());
246                }
247                crate::types::event::Event::Divorce if divorce_date.is_none() => {
248                    divorce_date = event.date.as_ref().and_then(|d| d.value.as_deref());
249                }
250                crate::types::event::Event::Annulment if annulment_date.is_none() => {
251                    annulment_date = event.date.as_ref().and_then(|d| d.value.as_deref());
252                }
253                _ => {}
254            }
255        }
256
257        if let Some(date) = marriage_date {
258            write!(f, ", m. {date}")?;
259        } else if let Some(date) = engagement_date {
260            write!(f, ", rel. {date}")?;
261        } else if let Some(date) = separated_date {
262            write!(f, ", sep. {date}")?;
263        } else if let Some(date) = divorce_date {
264            write!(f, ", div. {date}")?;
265        } else if let Some(date) = annulment_date {
266            write!(f, ", anul. {date}")?;
267        }
268
269        Ok(())
270    }
271}
272
273impl fmt::Display for Source {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        if let Some(ref xref) = self.xref {
276            write!(f, "{xref} ")?;
277        }
278
279        if let Some(ref title) = self.title {
280            write!(f, "\"{title}\"")?;
281        } else if let Some(ref abbr) = self.abbreviation {
282            write!(f, "{abbr}")?;
283        } else {
284            write!(f, "(Untitled Source)")?;
285        }
286
287        if let Some(ref author) = self.author {
288            write!(f, " by {author}")?;
289        }
290
291        Ok(())
292    }
293}
294
295impl fmt::Display for Repository {
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        if let Some(ref xref) = self.xref {
298            write!(f, "{xref} ")?;
299        }
300
301        if let Some(ref name) = self.name {
302            write!(f, "{name}")?;
303        } else {
304            write!(f, "(Unnamed Repository)")?;
305        }
306
307        if let Some(ref address) = self.address {
308            if let Some(ref city) = address.city {
309                write!(f, ", {city}")?;
310            }
311        }
312
313        Ok(())
314    }
315}
316
317impl fmt::Display for Multimedia {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        if let Some(ref xref) = self.xref {
320            write!(f, "{xref} ")?;
321        }
322
323        if let Some(ref title) = self.title {
324            write!(f, "\"{title}\"")?;
325        } else if let Some(ref file) = self.file {
326            if let Some(ref file_value) = file.value {
327                write!(f, "{file_value}")?;
328            } else {
329                write!(f, "(File reference)")?;
330            }
331        } else {
332            write!(f, "(Unnamed Media)")?;
333        }
334
335        if let Some(ref form) = self.form {
336            if let Some(ref format_value) = form.value {
337                write!(f, " [{format_value}]")?;
338            }
339        }
340
341        Ok(())
342    }
343}
344
345impl fmt::Display for Submitter {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        if let Some(ref xref) = self.xref {
348            write!(f, "{xref} ")?;
349        }
350
351        if let Some(ref name) = self.name {
352            write!(f, "{name}")?;
353        } else {
354            write!(f, "(Unknown Submitter)")?;
355        }
356
357        Ok(())
358    }
359}
360
361impl fmt::Display for Submission {
362    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363        if let Some(ref xref) = self.xref {
364            write!(f, "{xref} ")?;
365        }
366
367        if let Some(ref family_file) = self.family_file_name {
368            write!(f, "Family File: {family_file}")?;
369        } else {
370            write!(f, "(Submission Record)")?;
371        }
372
373        Ok(())
374    }
375}
376
377impl fmt::Display for Note {
378    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379        if let Some(ref value) = self.value {
380            // Truncate long notes for display
381            const MAX_LEN: usize = 100;
382            if value.len() > MAX_LEN {
383                write!(f, "{}...", &value[..MAX_LEN])?;
384            } else {
385                write!(f, "{value}")?;
386            }
387        } else {
388            write!(f, "(Empty Note)")?;
389        }
390
391        Ok(())
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::Gedcom;
399
400    #[test]
401    fn test_gedcom_data_display() {
402        let sample = "\
403            0 HEAD\n\
404            1 GEDC\n\
405            2 VERS 5.5\n\
406            0 @I1@ INDI\n\
407            1 NAME John /Doe/\n\
408            1 SEX M\n\
409            0 TRLR";
410
411        let mut gedcom = Gedcom::new(sample.chars()).unwrap();
412        let data = gedcom.parse_data().unwrap();
413
414        let display = format!("{data}");
415        assert!(display.contains("GEDCOM Data"));
416        assert!(display.contains("Individuals (1)"));
417        assert!(display.contains("John Doe"));
418    }
419
420    #[test]
421    fn test_individual_display() {
422        let sample = "\
423            0 HEAD\n\
424            1 GEDC\n\
425            2 VERS 5.5\n\
426            0 @I1@ INDI\n\
427            1 NAME Jane /Smith/\n\
428            1 SEX F\n\
429            1 BIRT\n\
430            2 DATE 15 MAR 1985\n\
431            0 TRLR";
432
433        let mut gedcom = Gedcom::new(sample.chars()).unwrap();
434        let data = gedcom.parse_data().unwrap();
435
436        let display = format!("{}", data.individuals[0]);
437        assert!(display.contains("@I1@"));
438        assert!(display.contains("Jane Smith"));
439        assert!(display.contains("Female"));
440        assert!(display.contains("b. 15 MAR 1985"));
441    }
442
443    #[test]
444    fn test_family_display() {
445        let sample = "\
446            0 HEAD\n\
447            1 GEDC\n\
448            2 VERS 5.5\n\
449            0 @F1@ FAM\n\
450            1 HUSB @I1@\n\
451            1 WIFE @I2@\n\
452            1 CHIL @I3@\n\
453            1 MARR\n\
454            2 DATE 1 JUN 2000\n\
455            0 TRLR";
456
457        let mut gedcom = Gedcom::new(sample.chars()).unwrap();
458        let data = gedcom.parse_data().unwrap();
459
460        let display = format!("{}", data.families[0]);
461        assert!(display.contains("@F1@"));
462        assert!(display.contains("@I1@"));
463        assert!(display.contains("@I2@"));
464        assert!(display.contains("1 child(ren)"));
465        assert!(display.contains("m. 1 JUN 2000"));
466    }
467
468    #[test]
469    fn test_family_display_engagement_fallback() {
470        let sample = "\
471            0 HEAD\n\
472            1 GEDC\n\
473            2 VERS 5.5\n\
474            0 @F1@ FAM\n\
475            1 HUSB @I1@\n\
476            1 WIFE @I2@\n\
477            1 ENGA\n\
478            2 DATE 1 JUN 1999\n\
479            0 TRLR";
480
481        let mut gedcom = Gedcom::new(sample.chars()).unwrap();
482        let data = gedcom.parse_data().unwrap();
483
484        let display = format!("{}", data.families[0]);
485        assert!(display.contains("rel. 1 JUN 1999"));
486    }
487
488    #[test]
489    fn test_family_display_separated_fallback() {
490        let sample = "\
491            0 HEAD\n\
492            1 GEDC\n\
493            2 VERS 5.5\n\
494            0 @F1@ FAM\n\
495            1 HUSB @I1@\n\
496            1 WIFE @I2@\n\
497            1 SEP\n\
498            2 DATE 1 JUN 2001\n\
499            0 TRLR";
500
501        let mut gedcom = Gedcom::new(sample.chars()).unwrap();
502        let data = gedcom.parse_data().unwrap();
503
504        let display = format!("{}", data.families[0]);
505        assert!(display.contains("sep. 1 JUN 2001"));
506    }
507
508    #[test]
509    fn test_family_display_divorce_fallback() {
510        let sample = "\
511            0 HEAD\n\
512            1 GEDC\n\
513            2 VERS 5.5\n\
514            0 @F1@ FAM\n\
515            1 HUSB @I1@\n\
516            1 WIFE @I2@\n\
517            1 DIV\n\
518            2 DATE 1 JUN 2002\n\
519            0 TRLR";
520
521        let mut gedcom = Gedcom::new(sample.chars()).unwrap();
522        let data = gedcom.parse_data().unwrap();
523
524        let display = format!("{}", data.families[0]);
525        assert!(display.contains("div. 1 JUN 2002"));
526    }
527
528    #[test]
529    fn test_family_display_annulment_fallback() {
530        let sample = "\
531            0 HEAD\n\
532            1 GEDC\n\
533            2 VERS 5.5\n\
534            0 @F1@ FAM\n\
535            1 HUSB @I1@\n\
536            1 WIFE @I2@\n\
537            1 ANUL\n\
538            2 DATE 1 JUN 2003\n\
539            0 TRLR";
540
541        let mut gedcom = Gedcom::new(sample.chars()).unwrap();
542        let data = gedcom.parse_data().unwrap();
543
544        let display = format!("{}", data.families[0]);
545        assert!(display.contains("anul. 1 JUN 2003"));
546    }
547
548    #[test]
549    fn test_name_display() {
550        let sample = "\
551            0 HEAD\n\
552            1 GEDC\n\
553            2 VERS 5.5\n\
554            0 @I1@ INDI\n\
555            1 NAME Robert /Johnson/ Jr.\n\
556            0 TRLR";
557
558        let mut gedcom = Gedcom::new(sample.chars()).unwrap();
559        let data = gedcom.parse_data().unwrap();
560
561        let name = data.individuals[0].name.as_ref().unwrap();
562        let display = format!("{name}");
563        assert!(display.contains("Robert"));
564        assert!(display.contains("Johnson"));
565        // Slashes should be removed
566        assert!(!display.contains('/'));
567    }
568
569    #[test]
570    fn test_source_display() {
571        let sample = "\
572            0 HEAD\n\
573            1 GEDC\n\
574            2 VERS 5.5\n\
575            0 @S1@ SOUR\n\
576            1 TITL Census Records 1900\n\
577            1 AUTH Government\n\
578            0 TRLR";
579
580        let mut gedcom = Gedcom::new(sample.chars()).unwrap();
581        let data = gedcom.parse_data().unwrap();
582
583        let display = format!("{}", data.sources[0]);
584        assert!(display.contains("@S1@"));
585        assert!(display.contains("Census Records 1900"));
586        assert!(display.contains("by Government"));
587    }
588
589    #[test]
590    fn test_header_display() {
591        let sample = "\
592            0 HEAD\n\
593            1 GEDC\n\
594            2 VERS 5.5\n\
595            1 SOUR MyApp\n\
596            2 NAME My Application\n\
597            1 CHAR UTF-8\n\
598            0 TRLR";
599
600        let mut gedcom = Gedcom::new(sample.chars()).unwrap();
601        let data = gedcom.parse_data().unwrap();
602
603        let header = data.header.as_ref().unwrap();
604        let display = format!("{header}");
605        assert!(display.contains("Header"));
606        assert!(display.contains("GEDCOM 5.5"));
607    }
608
609    #[test]
610    fn test_note_display_truncation() {
611        let long_note = "A".repeat(200);
612        let note = Note {
613            value: Some(long_note),
614            mime: None,
615            translation: None,
616            citation: None,
617            language: None,
618        };
619
620        let display = format!("{note}");
621        assert!(display.ends_with("..."));
622        assert!(display.len() < 110); // 100 chars + "..."
623    }
624}