gedcomkit 0.1.13__tar.gz → 0.1.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/CHANGELOG.md +18 -0
  2. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/Cargo.lock +1 -1
  3. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/Cargo.toml +1 -1
  4. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/PKG-INFO +1 -1
  5. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/bindings/gedcomkit-py/Cargo.lock +2 -2
  6. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/bindings/gedcomkit-py/Cargo.toml +1 -1
  7. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/build.rs +1 -2
  8. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/lib.rs +43 -4
  9. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/view.rs +61 -2
  10. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/LICENSE +0 -0
  11. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/README.md +0 -0
  12. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/bindings/gedcomkit-py/LICENSE +0 -0
  13. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/bindings/gedcomkit-py/README.md +0 -0
  14. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/bindings/gedcomkit-py/gedcomkit.pyi +0 -0
  15. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/bindings/gedcomkit-py/src/lib.rs +0 -0
  16. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom-spec/LICENSE +0 -0
  17. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom-spec/README.md +0 -0
  18. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom-spec/cardinalities.tsv +0 -0
  19. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom-spec/enumerations.tsv +0 -0
  20. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom-spec/enumerationsets.tsv +0 -0
  21. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom-spec/grammar.abnf +0 -0
  22. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom-spec/grammar.gedstruct +0 -0
  23. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom-spec/payloads.tsv +0 -0
  24. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom-spec/substructures.tsv +0 -0
  25. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/age-invalid.ged +0 -0
  26. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/age-valid.ged +0 -0
  27. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/atsign.ged +0 -0
  28. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/char_ascii_1.ged +0 -0
  29. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/char_ascii_2.ged +0 -0
  30. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/char_utf16be-1.ged +0 -0
  31. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/char_utf16be-2.ged +0 -0
  32. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/char_utf16le-1.ged +0 -0
  33. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/char_utf16le-2.ged +0 -0
  34. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/char_utf8-1.ged +0 -0
  35. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/char_utf8-2.ged +0 -0
  36. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/char_utf8-3.ged +0 -0
  37. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/date-all.ged +0 -0
  38. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/date-dual-invalid.ged +0 -0
  39. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/date-dual-valid.ged +0 -0
  40. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/enum-ext.ged +0 -0
  41. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/filename-1.ged +0 -0
  42. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/lang-all.ged +0 -0
  43. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/notes-1.ged +0 -0
  44. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/obje-1.ged +0 -0
  45. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/obsolete-1.ged +0 -0
  46. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/pedi-1.ged +0 -0
  47. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/rela_1.ged +0 -0
  48. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/sour-1.ged +0 -0
  49. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/tiny-1.ged +0 -0
  50. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/5/xref-case.ged +0 -0
  51. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/age-invalid.ged +0 -0
  52. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/age-valid.ged +0 -0
  53. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/atsign.ged +0 -0
  54. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/char_ascii_1.ged +0 -0
  55. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/char_ascii_2.ged +0 -0
  56. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/char_utf16be-1.ged +0 -0
  57. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/char_utf16be-2.ged +0 -0
  58. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/char_utf16le-1.ged +0 -0
  59. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/char_utf16le-2.ged +0 -0
  60. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/char_utf8-1.ged +0 -0
  61. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/char_utf8-2.ged +0 -0
  62. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/char_utf8-3.ged +0 -0
  63. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/date-all.ged +0 -0
  64. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/date-dual-invalid.ged +0 -0
  65. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/enum-ext.ged +0 -0
  66. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/filename-1.ged +0 -0
  67. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/lang-all.ged +0 -0
  68. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/notes-1.ged +0 -0
  69. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/obje-1.ged +0 -0
  70. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/obsolete-1.ged +0 -0
  71. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/pedi-1.ged +0 -0
  72. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/rela_1.ged +0 -0
  73. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/sour-1.ged +0 -0
  74. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/tiny-1.ged +0 -0
  75. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/7/xref-case.ged +0 -0
  76. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/LICENSE +0 -0
  77. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/gedcom7code/UPSTREAM-README.md +0 -0
  78. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/zip-interop/README.md +0 -0
  79. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/zip-interop/python-zip64.gdz +0 -0
  80. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/fixtures/vendored/zip-interop/python-zipfile.gdz +0 -0
  81. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/pyproject.toml +0 -0
  82. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/convert.rs +0 -0
  83. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/dates.rs +0 -0
  84. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/decode.rs +0 -0
  85. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/family.rs +0 -0
  86. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/gedzip.rs +0 -0
  87. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/names.rs +0 -0
  88. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/schema.rs +0 -0
  89. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/synthetic.rs +0 -0
  90. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/tags.rs +0 -0
  91. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/src/validate.rs +0 -0
  92. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/tests/endurance.rs +0 -0
  93. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/tests/external_charset.rs +0 -0
  94. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/tests/properties.rs +0 -0
  95. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/tests/requirement_corpus.rs +0 -0
  96. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/tests/spec_conformance.rs +0 -0
  97. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/tests/typescript.rs +0 -0
  98. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/tests/vendored_corpus.rs +0 -0
  99. {gedcomkit-0.1.13 → gedcomkit-0.1.14}/tests/version_conversion.rs +0 -0
@@ -5,6 +5,24 @@ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
5
5
  the project uses semantic versioning within the usual pre-1.0 compatibility
6
6
  rules.
7
7
 
8
+ ## [0.1.14] - 2026-10-10
9
+
10
+ ### Fixed
11
+
12
+ - `IndividualView::life_years` now reads the birth year from the first
13
+ `BIRT` that has a year, else the first `CHR` with one, else the first
14
+ `BAPM`. Before, it looked only at `BIRT` then `CHR`, and at the first event
15
+ with the tag even when its date did not parse, so a dateless `BIRT` ahead of
16
+ a dated one gave no year. `IndividualView::event_year` is unchanged. The
17
+ death year is still `DEAT` only.
18
+
19
+ ### Changed
20
+
21
+ - `Document::record` and `Document::record_mut` no longer allocate an
22
+ upper-cased copy of every identifier they scan; they compare ASCII
23
+ case-insensitively in place. Matching is identical to before (ASCII case
24
+ folding only, first duplicate wins) and to `Document::xref_index`.
25
+
8
26
  ## [0.1.13] - 2026-10-02
9
27
 
10
28
  ### Fixed
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.13"
38
+ version = "0.1.14"
39
39
  dependencies = [
40
40
  "flate2",
41
41
  "serde",
@@ -3,7 +3,7 @@
3
3
  # was checked on crates.io, npm, and PyPI before the initial release.
4
4
  [package]
5
5
  name = "gedcomkit"
6
- version = "0.1.13"
6
+ version = "0.1.14"
7
7
  description = "A byte-preserving GEDCOM document model: decoding, parsing, readings, version conversion, plausibility checks, and the GEDZIP container, for GEDCOM 5.5 through 7.x."
8
8
  edition = "2024"
9
9
  # The true minimum, not the workspace's toolchain: a parser library is a leaf
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gedcomkit
3
- Version: 0.1.13
3
+ Version: 0.1.14
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.13"
38
+ version = "0.1.14"
39
39
  dependencies = [
40
40
  "flate2",
41
41
  "serde",
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  [[package]]
45
45
  name = "gedcomkit-py"
46
- version = "0.1.13"
46
+ version = "0.1.14"
47
47
  dependencies = [
48
48
  "gedcomkit",
49
49
  "pyo3",
@@ -2,7 +2,7 @@
2
2
  # abi3 keeps it to one wheel per platform across Python versions.
3
3
  [package]
4
4
  name = "gedcomkit-py"
5
- version = "0.1.13"
5
+ version = "0.1.14"
6
6
  description = "Python bindings for gedcomkit: an archival-grade GEDCOM library — byte-preserving round-trip, encoding detection, GEDCOM 5.5 through 7.x, GEDZIP."
7
7
  edition = "2024"
8
8
  rust-version = "1.88"
@@ -455,14 +455,13 @@ impl Document {
455
455
  ),
456
456
  ));
457
457
  }
458
- let wanted = record.to_ascii_uppercase();
459
458
  let owner = self
460
459
  .records
461
460
  .iter_mut()
462
461
  .find(|node| {
463
462
  node.xref.as_ref().map_or_else(
464
463
  || node.tag == record,
465
- |xref| xref.to_ascii_uppercase() == wanted,
464
+ |xref| xref.eq_ignore_ascii_case(record),
466
465
  )
467
466
  })
468
467
  .ok_or_else(|| fault(0, GedcomErrorKind::Structure, format!("no record {record}")))?;
@@ -847,23 +847,24 @@ impl Document {
847
847
  /// case and `xref-case.ged` in the fixture corpus proves it matters.
848
848
  #[must_use]
849
849
  pub fn record(&self, xref: &str) -> Option<&Node> {
850
- let wanted = xref.to_ascii_uppercase();
850
+ // `eq_ignore_ascii_case` is exactly "equal after `to_ascii_uppercase`"
851
+ // (non-ASCII bytes compare as themselves), without allocating an
852
+ // upper-cased copy of every identifier scanned.
851
853
  self.records.iter().find(|record| {
852
854
  record
853
855
  .xref
854
856
  .as_ref()
855
- .is_some_and(|own| own.to_ascii_uppercase() == wanted)
857
+ .is_some_and(|own| own.eq_ignore_ascii_case(xref))
856
858
  })
857
859
  }
858
860
 
859
861
  /// The record with this cross-reference identifier, mutably.
860
862
  pub fn record_mut(&mut self, xref: &str) -> Option<&mut Node> {
861
- let wanted = xref.to_ascii_uppercase();
862
863
  self.records.iter_mut().find(|record| {
863
864
  record
864
865
  .xref
865
866
  .as_ref()
866
- .is_some_and(|own| own.to_ascii_uppercase() == wanted)
867
+ .is_some_and(|own| own.eq_ignore_ascii_case(xref))
867
868
  })
868
869
  }
869
870
 
@@ -1297,6 +1298,44 @@ mod tests {
1297
1298
  }
1298
1299
  }
1299
1300
 
1301
+ #[test]
1302
+ fn lookup_folds_ascii_case_only_and_the_first_duplicate_wins() {
1303
+ let document = Document::parse(
1304
+ "0 HEAD\n0 @a1@ INDI\n1 NAME First\n0 @A1@ INDI\n1 NAME Second\n\
1305
+ 0 @\u{e9}1@ INDI\n0 @\u{c9}1@ INDI\n1 NAME Upper\n0 TRLR\n",
1306
+ )
1307
+ .expect("parse");
1308
+
1309
+ // Case-insensitive for ASCII, and the earlier duplicate is the one found.
1310
+ for wanted in ["@a1@", "@A1@"] {
1311
+ let found = document.record(wanted).expect("found");
1312
+ assert_eq!(found.xref.as_deref(), Some("@a1@"));
1313
+ }
1314
+ // Non-ASCII letters are not case-folded, matching `xref_index`.
1315
+ assert_eq!(
1316
+ document
1317
+ .record("@\u{e9}1@")
1318
+ .and_then(|node| node.xref.as_deref()),
1319
+ Some("@\u{e9}1@")
1320
+ );
1321
+ assert_eq!(
1322
+ document
1323
+ .record("@\u{c9}1@")
1324
+ .and_then(|node| node.xref.as_deref()),
1325
+ Some("@\u{c9}1@")
1326
+ );
1327
+ assert!(document.record("@z9@").is_none());
1328
+
1329
+ let index = document.xref_index();
1330
+ assert_eq!(index["@A1@"], 1);
1331
+ assert_ne!(index["@\u{c9}1@"], index["@\u{e9}1@"]);
1332
+
1333
+ let mut document = document;
1334
+ let found = document.record_mut("@A1@").expect("found mutably");
1335
+ assert_eq!(found.xref.as_deref(), Some("@a1@"));
1336
+ assert!(document.record_mut("@\u{c9}1@").is_some());
1337
+ }
1338
+
1300
1339
  #[test]
1301
1340
  fn next_xref_continues_the_files_own_numbering() {
1302
1341
  let document = Document::parse("0 HEAD\n0 @I1@ INDI\n0 @I7@ INDI\n0 @F1@ FAM\n0 TRLR\n")
@@ -452,11 +452,30 @@ impl IndividualView {
452
452
  .and_then(|event| event.year)
453
453
  }
454
454
 
455
+ /// The year of the first event with this tag whose date parsed.
456
+ ///
457
+ /// [`IndividualView::event_year`] stops at the first event with the tag,
458
+ /// so a dateless `BIRT` hides a dated one after it; a displayed birth year
459
+ /// should not be lost that way. That method keeps its documented meaning
460
+ /// and this private helper serves [`IndividualView::life_years`].
461
+ fn first_year(&self, tag: &str) -> Option<i32> {
462
+ self.events
463
+ .iter()
464
+ .filter(|event| event.tag == tag)
465
+ .find_map(|event| event.year)
466
+ }
467
+
455
468
  /// The years to show beside a name: birth and death, when they parsed.
469
+ ///
470
+ /// The birth year is the first `BIRT` with a year, else the first `CHR`
471
+ /// with one, else the first `BAPM`; the death year is `DEAT` only.
456
472
  #[must_use]
457
473
  pub fn life_years(&self) -> Option<String> {
458
- let year = |tag: &str| self.event_year(tag);
459
- match (year("BIRT").or_else(|| year("CHR")), year("DEAT")) {
474
+ let birth = self
475
+ .first_year("BIRT")
476
+ .or_else(|| self.first_year("CHR"))
477
+ .or_else(|| self.first_year("BAPM"));
478
+ match (birth, self.first_year("DEAT")) {
460
479
  (Some(birth), Some(death)) => Some(format!("{birth}–{death}")),
461
480
  (Some(birth), None) => Some(format!("b. {birth}")),
462
481
  (None, Some(death)) => Some(format!("d. {death}")),
@@ -634,6 +653,46 @@ mod tests {
634
653
  assert_eq!(view.identifiers[0].kind.as_deref(), Some("FamilySearch"));
635
654
  }
636
655
 
656
+ fn life_years_of(body: &str) -> Option<String> {
657
+ let text = format!("0 HEAD\n0 @I1@ INDI\n{body}0 TRLR\n");
658
+ let document = Document::parse(&text).expect("parse");
659
+ IndividualView::from_node(document.record("@I1@").expect("individual")).life_years()
660
+ }
661
+
662
+ #[test]
663
+ fn a_birth_year_skips_events_whose_date_gave_none() {
664
+ // A dateless BIRT before a dated one still yields the year.
665
+ let body = "1 BIRT\n1 BIRT\n2 DATE 1850\n";
666
+ assert_eq!(life_years_of(body).as_deref(), Some("b. 1850"));
667
+ // An unparsed date is skipped the same way.
668
+ let body = "1 BIRT\n2 DATE nonsense\n1 BIRT\n2 DATE 1851\n";
669
+ assert_eq!(life_years_of(body).as_deref(), Some("b. 1851"));
670
+ // event_year keeps its documented first-event meaning.
671
+ let document =
672
+ Document::parse("0 HEAD\n0 @I1@ INDI\n1 BIRT\n1 BIRT\n2 DATE 1850\n0 TRLR\n")
673
+ .expect("parse");
674
+ let view = IndividualView::from_node(document.record("@I1@").expect("individual"));
675
+ assert_eq!(view.event_year("BIRT"), None);
676
+ }
677
+
678
+ #[test]
679
+ fn a_birth_year_falls_back_from_birth_to_christening_to_baptism() {
680
+ let bapm = "1 BAPM\n2 DATE 1802\n";
681
+ assert_eq!(life_years_of(bapm).as_deref(), Some("b. 1802"));
682
+ let both = "1 BAPM\n2 DATE 1802\n1 CHR\n2 DATE 1801\n";
683
+ assert_eq!(life_years_of(both).as_deref(), Some("b. 1801"));
684
+ let all = "1 BAPM\n2 DATE 1802\n1 CHR\n2 DATE 1801\n1 BIRT\n2 DATE 1800\n";
685
+ assert_eq!(life_years_of(all).as_deref(), Some("b. 1800"));
686
+ // A dateless BIRT does not block the fallback.
687
+ let dateless = "1 BIRT\n1 CHR\n2 DATE 1801\n";
688
+ assert_eq!(life_years_of(dateless).as_deref(), Some("b. 1801"));
689
+ // Death stays DEAT only: a burial year is not a death year.
690
+ let burial = "1 BAPM\n2 DATE 1802\n1 BURI\n2 DATE 1870\n";
691
+ assert_eq!(life_years_of(burial).as_deref(), Some("b. 1802"));
692
+ let died = "1 BAPM\n2 DATE 1802\n1 DEAT\n2 DATE 1870\n";
693
+ assert_eq!(life_years_of(died).as_deref(), Some("1802–1870"));
694
+ }
695
+
637
696
  #[test]
638
697
  fn a_record_type_we_do_not_know_still_has_a_title_and_a_view() {
639
698
  let record = Document::parse("0 HEAD\n0 @X1@ _OPAQUE\n1 QUERY Tova Testwright\n0 TRLR\n")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes