gedcomkit 0.1.7__tar.gz → 0.1.9__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.
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/CHANGELOG.md +37 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/Cargo.lock +1 -1
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/Cargo.toml +1 -1
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/PKG-INFO +1 -1
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/bindings/gedcomkit-py/Cargo.lock +2 -2
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/bindings/gedcomkit-py/Cargo.toml +1 -1
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/validate.rs +319 -49
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/LICENSE +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/README.md +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/bindings/gedcomkit-py/LICENSE +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/bindings/gedcomkit-py/README.md +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/bindings/gedcomkit-py/gedcomkit.pyi +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/bindings/gedcomkit-py/src/lib.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom-spec/LICENSE +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom-spec/README.md +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom-spec/cardinalities.tsv +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom-spec/enumerations.tsv +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom-spec/enumerationsets.tsv +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom-spec/grammar.abnf +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom-spec/grammar.gedstruct +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom-spec/payloads.tsv +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom-spec/substructures.tsv +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/age-invalid.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/age-valid.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/atsign.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/char_ascii_1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/char_ascii_2.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/char_utf16be-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/char_utf16be-2.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/char_utf16le-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/char_utf16le-2.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/char_utf8-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/char_utf8-2.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/char_utf8-3.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/date-all.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/date-dual-invalid.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/date-dual-valid.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/enum-ext.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/filename-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/lang-all.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/notes-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/obje-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/obsolete-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/pedi-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/rela_1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/sour-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/tiny-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/5/xref-case.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/age-invalid.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/age-valid.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/atsign.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/char_ascii_1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/char_ascii_2.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/char_utf16be-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/char_utf16be-2.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/char_utf16le-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/char_utf16le-2.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/char_utf8-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/char_utf8-2.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/char_utf8-3.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/date-all.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/date-dual-invalid.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/enum-ext.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/filename-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/lang-all.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/notes-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/obje-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/obsolete-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/pedi-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/rela_1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/sour-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/tiny-1.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/7/xref-case.ged +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/LICENSE +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/gedcom7code/UPSTREAM-README.md +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/zip-interop/README.md +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/zip-interop/python-zip64.gdz +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/fixtures/vendored/zip-interop/python-zipfile.gdz +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/pyproject.toml +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/build.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/convert.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/dates.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/decode.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/family.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/gedzip.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/lib.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/names.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/schema.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/synthetic.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/tags.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/src/view.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/tests/endurance.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/tests/external_charset.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/tests/properties.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/tests/requirement_corpus.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/tests/spec_conformance.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/tests/vendored_corpus.rs +0 -0
- {gedcomkit-0.1.7 → gedcomkit-0.1.9}/tests/version_conversion.rs +0 -0
|
@@ -5,6 +5,43 @@ 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.9] - 2026-09-27
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `validate::inspect` drew years from dates the reader could not read: a
|
|
13
|
+
year merely mentioned in an unreadable payload (`WFT Est 1804-1911`) was
|
|
14
|
+
taken as the event's year, contrary to the module's own rule, and made
|
|
15
|
+
contradictions out of nothing. Such dates are now left out of every check.
|
|
16
|
+
- A date that bounds a year rather than naming one (`BEF 1811`, `AFT 1850`,
|
|
17
|
+
`BET 1800 AND 1810`) was compared as if it named its one mentioned year.
|
|
18
|
+
Checks now compare the years a date admits, so a finding is reported only
|
|
19
|
+
when no year both dates allow could be true, and the message says the
|
|
20
|
+
bounds it was drawn from (`born 1807 and died before 1805`; a parent age
|
|
21
|
+
of `at most 10 years old`).
|
|
22
|
+
- A birth dated after a death was reported twice, as `died-before-born` and
|
|
23
|
+
as `event-after-death`; it is now reported once.
|
|
24
|
+
- A Hebrew or French Republican year was compared as if it were Gregorian;
|
|
25
|
+
such dates are now left out of the checks. A range written backwards
|
|
26
|
+
(`BET 1910 AND 1900`) admits the same years as one written forwards.
|
|
27
|
+
- `child-born-after-mother-died` was skipped whenever the mother's own birth
|
|
28
|
+
year was unknown, although it needs only her death and the child's birth;
|
|
29
|
+
it now runs whenever those two are known, and alongside a
|
|
30
|
+
`parent-too-young` finding rather than instead of one. A child born before
|
|
31
|
+
its parent is worded as such (`5 years before the parent was born`), and a
|
|
32
|
+
lifespan between two exact years is stated exactly.
|
|
33
|
+
|
|
34
|
+
## [0.1.8] - 2026-09-27
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- Python wheels for Windows (x86_64) and macOS (Apple silicon and Intel)
|
|
39
|
+
on PyPI, beside the Linux wheel, so installing on those platforms no
|
|
40
|
+
longer compiles Rust from the source distribution. All three are
|
|
41
|
+
cross-compiled on the Linux release runner — macOS with zig, Windows with
|
|
42
|
+
cargo-xwin — and, being abi3, each serves Python 3.9 onward. (PyO3 links
|
|
43
|
+
`python3.dll` by raw-dylib, so the Windows build needs no Windows Python.)
|
|
44
|
+
|
|
8
45
|
## [0.1.7] - 2026-09-27
|
|
9
46
|
|
|
10
47
|
### Added
|
|
@@ -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.
|
|
6
|
+
version = "0.1.9"
|
|
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
|
|
@@ -35,7 +35,7 @@ dependencies = [
|
|
|
35
35
|
|
|
36
36
|
[[package]]
|
|
37
37
|
name = "gedcomkit"
|
|
38
|
-
version = "0.1.
|
|
38
|
+
version = "0.1.9"
|
|
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.
|
|
46
|
+
version = "0.1.9"
|
|
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.
|
|
5
|
+
version = "0.1.9"
|
|
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"
|
|
@@ -20,12 +20,18 @@
|
|
|
20
20
|
//! **A date nobody can read is not a contradiction.** Half the dates in a real
|
|
21
21
|
//! file are approximate, and a check that treats "about 1850" as a claim about
|
|
22
22
|
//! a particular day produces a page of findings nobody will read twice. Only
|
|
23
|
-
//! years are compared, and only where
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
//! years are compared, and only where the date reader recovered a reading: a
|
|
24
|
+
//! year merely *mentioned* in an unreadable payload (`WFT Est 1804-1911`) says
|
|
25
|
+
//! nothing about when the event happened. A date that bounds a year rather
|
|
26
|
+
//! than naming one — `BEF 1811`, `AFT 1850`, `BET 1804 AND 1911` — is
|
|
27
|
+
//! compared by its bounds, so it contradicts another only when no year it
|
|
28
|
+
//! admits could be true.
|
|
29
|
+
|
|
30
|
+
use crate::dates::{Calendar, GedcomDate};
|
|
26
31
|
use crate::view::{FamilyView, IndividualView, record_title};
|
|
27
32
|
use crate::{Document, Node};
|
|
28
33
|
use std::collections::HashMap;
|
|
34
|
+
use std::fmt;
|
|
29
35
|
|
|
30
36
|
/// How much a finding is claiming.
|
|
31
37
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
|
@@ -81,12 +87,81 @@ const OLDEST_FATHER: i32 = 80;
|
|
|
81
87
|
/// A parent younger than this at a birth is almost always a mis-keyed year.
|
|
82
88
|
const YOUNGEST_PARENT: i32 = 12;
|
|
83
89
|
|
|
90
|
+
/// The years a date admits: its earliest and latest, either open when the
|
|
91
|
+
/// date bounds only one side (`BEF 1811` has no earliest).
|
|
92
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
93
|
+
struct Years {
|
|
94
|
+
earliest: Option<i32>,
|
|
95
|
+
latest: Option<i32>,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
impl Years {
|
|
99
|
+
/// The years a `DATE` payload admits, or `None` when the reader could not
|
|
100
|
+
/// read it — a year that merely appears in unreadable text is not used.
|
|
101
|
+
///
|
|
102
|
+
/// Only Gregorian and Julian years are comparable with each other; a
|
|
103
|
+
/// Hebrew or French Republican year counts from another epoch, so such a
|
|
104
|
+
/// date is left out rather than compared as if it were Gregorian. A range
|
|
105
|
+
/// written backwards (`BET 1910 AND 1900`) admits the same years as one
|
|
106
|
+
/// written forwards.
|
|
107
|
+
fn of(payload: &str) -> Option<Self> {
|
|
108
|
+
let date = GedcomDate::parse(payload);
|
|
109
|
+
if !date.is_parsed() || !matches!(date.calendar, Calendar::Gregorian | Calendar::Julian) {
|
|
110
|
+
return None;
|
|
111
|
+
}
|
|
112
|
+
let mut years = Self {
|
|
113
|
+
earliest: date.earliest.map(|point| point.year),
|
|
114
|
+
latest: date.latest.map(|point| point.year),
|
|
115
|
+
};
|
|
116
|
+
if let (Some(start), Some(end)) = (years.earliest, years.latest)
|
|
117
|
+
&& start > end
|
|
118
|
+
{
|
|
119
|
+
years = Self {
|
|
120
|
+
earliest: Some(end),
|
|
121
|
+
latest: Some(start),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
(years.earliest.is_some() || years.latest.is_some()).then_some(years)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Whether every year this admits comes before every year `other` admits.
|
|
128
|
+
const fn wholly_before(self, other: Self) -> bool {
|
|
129
|
+
matches!((self.latest, other.earliest), (Some(end), Some(start)) if end < start)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// The smallest and largest number of years from `self` to `later` that
|
|
133
|
+
/// the two dates allow, each `None` when a bound is open.
|
|
134
|
+
fn gap_to(self, later: Self) -> (Option<i32>, Option<i32>) {
|
|
135
|
+
let least = later
|
|
136
|
+
.earliest
|
|
137
|
+
.zip(self.latest)
|
|
138
|
+
.map(|(end, start)| end - start);
|
|
139
|
+
let most = later
|
|
140
|
+
.latest
|
|
141
|
+
.zip(self.earliest)
|
|
142
|
+
.map(|(end, start)| end - start);
|
|
143
|
+
(least, most)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
impl fmt::Display for Years {
|
|
148
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
149
|
+
match (self.earliest, self.latest) {
|
|
150
|
+
(Some(start), Some(end)) if start == end => write!(formatter, "{start}"),
|
|
151
|
+
(Some(start), Some(end)) => write!(formatter, "between {start} and {end}"),
|
|
152
|
+
(None, Some(end)) => write!(formatter, "before {end}"),
|
|
153
|
+
(Some(start), None) => write!(formatter, "after {start}"),
|
|
154
|
+
(None, None) => formatter.write_str("undated"),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
84
159
|
/// What one person's record says about when they lived.
|
|
85
160
|
#[derive(Clone, Copy, Default)]
|
|
86
161
|
struct Life {
|
|
87
|
-
born: Option<
|
|
88
|
-
died: Option<
|
|
89
|
-
buried: Option<
|
|
162
|
+
born: Option<Years>,
|
|
163
|
+
died: Option<Years>,
|
|
164
|
+
buried: Option<Years>,
|
|
90
165
|
}
|
|
91
166
|
|
|
92
167
|
/// Every finding in the document, in record order.
|
|
@@ -119,12 +194,12 @@ pub fn inspect(document: &Document) -> Vec<Finding> {
|
|
|
119
194
|
found
|
|
120
195
|
}
|
|
121
196
|
|
|
122
|
-
fn year_of(view: &IndividualView, tag: &str) -> Option<
|
|
197
|
+
fn year_of(view: &IndividualView, tag: &str) -> Option<Years> {
|
|
123
198
|
view.events
|
|
124
199
|
.iter()
|
|
125
200
|
.find(|event| event.tag == tag)
|
|
126
201
|
.and_then(|event| event.date.as_deref())
|
|
127
|
-
.and_then(
|
|
202
|
+
.and_then(Years::of)
|
|
128
203
|
}
|
|
129
204
|
|
|
130
205
|
fn life_of(view: &IndividualView) -> Life {
|
|
@@ -162,7 +237,7 @@ fn inspect_person(
|
|
|
162
237
|
.unwrap_or_default();
|
|
163
238
|
|
|
164
239
|
if let (Some(born), Some(died)) = (life.born, life.died) {
|
|
165
|
-
if died
|
|
240
|
+
if died.wholly_before(born) {
|
|
166
241
|
add(
|
|
167
242
|
found,
|
|
168
243
|
record,
|
|
@@ -170,22 +245,28 @@ fn inspect_person(
|
|
|
170
245
|
Severity::Contradiction,
|
|
171
246
|
format!("born {born} and died {died}"),
|
|
172
247
|
);
|
|
173
|
-
} else if
|
|
248
|
+
} else if let (Some(shortest), _) = born.gap_to(died)
|
|
249
|
+
&& shortest > LONGEST_LIFE
|
|
250
|
+
{
|
|
174
251
|
add(
|
|
175
252
|
found,
|
|
176
253
|
record,
|
|
177
254
|
"improbable-lifespan",
|
|
178
255
|
Severity::Question,
|
|
179
256
|
format!(
|
|
180
|
-
"born {born} and died {died}, which is {} years",
|
|
181
|
-
died
|
|
257
|
+
"born {born} and died {died}, which is {}{shortest} years",
|
|
258
|
+
if born.gap_to(died).1 == Some(shortest) {
|
|
259
|
+
""
|
|
260
|
+
} else {
|
|
261
|
+
"at least "
|
|
262
|
+
}
|
|
182
263
|
),
|
|
183
264
|
);
|
|
184
265
|
}
|
|
185
266
|
}
|
|
186
267
|
|
|
187
268
|
if let (Some(died), Some(buried)) = (life.died, life.buried)
|
|
188
|
-
&& buried
|
|
269
|
+
&& buried.wholly_before(died)
|
|
189
270
|
{
|
|
190
271
|
add(
|
|
191
272
|
found,
|
|
@@ -197,23 +278,20 @@ fn inspect_person(
|
|
|
197
278
|
}
|
|
198
279
|
|
|
199
280
|
// An event after the funeral is either a wrong year or a second person.
|
|
200
|
-
// Probate and a will are the two that legitimately come after a death
|
|
281
|
+
// Probate and a will are the two that legitimately come after a death;
|
|
282
|
+
// a birth after it is already `died-before-born`, reported once.
|
|
201
283
|
if let Some(died) = life.died {
|
|
202
284
|
for event in &view.events {
|
|
203
285
|
if matches!(
|
|
204
286
|
event.tag.as_str(),
|
|
205
|
-
"DEAT" | "BURI" | "CREM" | "PROB" | "WILL"
|
|
287
|
+
"BIRT" | "DEAT" | "BURI" | "CREM" | "PROB" | "WILL"
|
|
206
288
|
) {
|
|
207
289
|
continue;
|
|
208
290
|
}
|
|
209
|
-
let Some(year) = event
|
|
210
|
-
.date
|
|
211
|
-
.as_deref()
|
|
212
|
-
.and_then(|payload| GedcomDate::parse(payload).year_hint())
|
|
213
|
-
else {
|
|
291
|
+
let Some(year) = event.date.as_deref().and_then(Years::of) else {
|
|
214
292
|
continue;
|
|
215
293
|
};
|
|
216
|
-
if year
|
|
294
|
+
if died.wholly_before(year) {
|
|
217
295
|
add(
|
|
218
296
|
found,
|
|
219
297
|
record,
|
|
@@ -244,7 +322,7 @@ fn inspect_family(record: &Node, lives: &HashMap<String, Life>, found: &mut Vec<
|
|
|
244
322
|
let married = record
|
|
245
323
|
.first("MARR")
|
|
246
324
|
.and_then(|event| event.value_of("DATE"))
|
|
247
|
-
.and_then(|payload|
|
|
325
|
+
.and_then(|payload| Years::of(&payload));
|
|
248
326
|
|
|
249
327
|
for parent in family.partners() {
|
|
250
328
|
let Some(parent_life) = lives.get(&parent.to_ascii_uppercase()).copied() else {
|
|
@@ -293,30 +371,55 @@ fn inspect_family(record: &Node, lives: &HashMap<String, Life>, found: &mut Vec<
|
|
|
293
371
|
}
|
|
294
372
|
}
|
|
295
373
|
|
|
296
|
-
fn inspect_marriage(record: &Node, married: Option<
|
|
374
|
+
fn inspect_marriage(record: &Node, married: Option<Years>, parent: Life, found: &mut Vec<Finding>) {
|
|
297
375
|
let (Some(married), Some(born)) = (married, parent.born) else {
|
|
298
376
|
return;
|
|
299
377
|
};
|
|
300
|
-
|
|
301
|
-
if age < 0 {
|
|
378
|
+
if married.wholly_before(born) {
|
|
302
379
|
add(
|
|
303
380
|
found,
|
|
304
381
|
record,
|
|
305
382
|
"married-before-born",
|
|
306
383
|
Severity::Contradiction,
|
|
307
|
-
format!("a marriage
|
|
384
|
+
format!("a marriage {} for somebody born {born}", in_year(married)),
|
|
308
385
|
);
|
|
309
|
-
} else if
|
|
386
|
+
} else if let (_, Some(oldest)) = born.gap_to(married)
|
|
387
|
+
&& oldest < YOUNGEST_MARRIAGE
|
|
388
|
+
{
|
|
310
389
|
add(
|
|
311
390
|
found,
|
|
312
391
|
record,
|
|
313
392
|
"married-as-a-child",
|
|
314
393
|
Severity::Question,
|
|
315
|
-
format!(
|
|
394
|
+
format!(
|
|
395
|
+
"married {} {}, born {born}",
|
|
396
|
+
in_year(married),
|
|
397
|
+
age(born, married)
|
|
398
|
+
),
|
|
316
399
|
);
|
|
317
400
|
}
|
|
318
401
|
}
|
|
319
402
|
|
|
403
|
+
/// `in 1900`, or the bounded form (`before 1811`) as it stands.
|
|
404
|
+
fn in_year(years: Years) -> String {
|
|
405
|
+
match (years.earliest, years.latest) {
|
|
406
|
+
(Some(start), Some(end)) if start == end => format!("in {start}"),
|
|
407
|
+
_ => years.to_string(),
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/// The age between two dates as a phrase: `at 9` when both are single years,
|
|
412
|
+
/// the range the dates allow otherwise (`at 9 to 12`, `at most 9`).
|
|
413
|
+
fn age(from: Years, to: Years) -> String {
|
|
414
|
+
match from.gap_to(to) {
|
|
415
|
+
(Some(least), Some(most)) if least == most => format!("at {least}"),
|
|
416
|
+
(Some(least), Some(most)) => format!("at {least} to {most}"),
|
|
417
|
+
(Some(least), None) => format!("at least {least}"),
|
|
418
|
+
(None, Some(most)) => format!("at most {most}"),
|
|
419
|
+
(None, None) => "at an unknown age".to_owned(),
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
320
423
|
fn inspect_parenthood(
|
|
321
424
|
record: &Node,
|
|
322
425
|
parent: Life,
|
|
@@ -324,46 +427,64 @@ fn inspect_parenthood(
|
|
|
324
427
|
mother: bool,
|
|
325
428
|
found: &mut Vec<Finding>,
|
|
326
429
|
) {
|
|
327
|
-
let
|
|
430
|
+
let Some(child_born) = child.born else {
|
|
328
431
|
return;
|
|
329
432
|
};
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
433
|
+
|
|
434
|
+
// A father may die before a birth; a mother may not. This needs only her
|
|
435
|
+
// death and the child's birth, not her own birth year.
|
|
436
|
+
if let (true, Some(parent_died)) = (mother, parent.died)
|
|
437
|
+
&& parent_died.wholly_before(child_born)
|
|
438
|
+
{
|
|
333
439
|
add(
|
|
334
440
|
found,
|
|
335
441
|
record,
|
|
336
|
-
"
|
|
442
|
+
"child-born-after-mother-died",
|
|
337
443
|
Severity::Contradiction,
|
|
338
|
-
format!(
|
|
339
|
-
"a parent born {parent_born} with a child born {child_born}, {how} {} years old",
|
|
340
|
-
age.abs()
|
|
341
|
-
),
|
|
444
|
+
format!("a mother who died {parent_died} with a child born {child_born}"),
|
|
342
445
|
);
|
|
343
|
-
return;
|
|
344
446
|
}
|
|
345
447
|
|
|
346
|
-
let
|
|
347
|
-
|
|
448
|
+
let Some(parent_born) = parent.born else {
|
|
449
|
+
return;
|
|
450
|
+
};
|
|
451
|
+
let (youngest, oldest) = parent_born.gap_to(child_born);
|
|
452
|
+
// Too young only if even the oldest age the dates allow is too young.
|
|
453
|
+
if let Some(oldest) = oldest
|
|
454
|
+
&& oldest < YOUNGEST_PARENT
|
|
455
|
+
{
|
|
456
|
+
// The sign decides the wording: a negative oldest age means the child
|
|
457
|
+
// was born before the parent whatever the dates' ranges.
|
|
458
|
+
let said = if oldest < 0 {
|
|
459
|
+
format!("{} years before the parent was born", oldest.abs())
|
|
460
|
+
} else if youngest == Some(oldest) {
|
|
461
|
+
format!("at {oldest} years old")
|
|
462
|
+
} else {
|
|
463
|
+
format!("at most {oldest} years old")
|
|
464
|
+
};
|
|
348
465
|
add(
|
|
349
466
|
found,
|
|
350
467
|
record,
|
|
351
|
-
"parent-too-
|
|
352
|
-
Severity::
|
|
353
|
-
format!("a parent born {parent_born} with a child born {child_born},
|
|
468
|
+
"parent-too-young",
|
|
469
|
+
Severity::Contradiction,
|
|
470
|
+
format!("a parent born {parent_born} with a child born {child_born}, {said}"),
|
|
354
471
|
);
|
|
472
|
+
return;
|
|
355
473
|
}
|
|
356
474
|
|
|
357
|
-
|
|
358
|
-
if let
|
|
359
|
-
&&
|
|
475
|
+
let limit = if mother { OLDEST_MOTHER } else { OLDEST_FATHER };
|
|
476
|
+
if let Some(youngest) = youngest
|
|
477
|
+
&& youngest > limit
|
|
360
478
|
{
|
|
361
479
|
add(
|
|
362
480
|
found,
|
|
363
481
|
record,
|
|
364
|
-
"
|
|
365
|
-
Severity::
|
|
366
|
-
format!(
|
|
482
|
+
"parent-too-old",
|
|
483
|
+
Severity::Question,
|
|
484
|
+
format!(
|
|
485
|
+
"a parent born {parent_born} with a child born {child_born}, {}",
|
|
486
|
+
age(parent_born, child_born)
|
|
487
|
+
),
|
|
367
488
|
);
|
|
368
489
|
}
|
|
369
490
|
}
|
|
@@ -549,6 +670,155 @@ mod tests {
|
|
|
549
670
|
);
|
|
550
671
|
}
|
|
551
672
|
|
|
673
|
+
#[test]
|
|
674
|
+
fn a_year_mentioned_in_an_unreadable_date_is_not_a_death_year() {
|
|
675
|
+
// `WFT Est 1804-1911` mentions 1804; it does not say the person died
|
|
676
|
+
// then, so a birth in 1807 contradicts nothing.
|
|
677
|
+
let found = findings(
|
|
678
|
+
"0 HEAD\n0 @I1@ INDI\n1 BIRT\n2 DATE 1807\n1 DEAT\n2 DATE WFT Est 1804-1911\n0 TRLR\n",
|
|
679
|
+
);
|
|
680
|
+
assert!(
|
|
681
|
+
!checks(&found).contains(&"died-before-born")
|
|
682
|
+
&& !checks(&found).contains(&"event-after-death"),
|
|
683
|
+
"{found:?}"
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
#[test]
|
|
688
|
+
fn a_bounded_date_contradicts_only_when_no_year_it_allows_could_be_true() {
|
|
689
|
+
// Died before 1811 and born 1807: both can be true.
|
|
690
|
+
let found =
|
|
691
|
+
findings("0 HEAD\n0 @I1@ INDI\n1 BIRT\n2 DATE 1807\n1 DEAT\n2 DATE BEF 1811\n0 TRLR\n");
|
|
692
|
+
assert!(!checks(&found).contains(&"died-before-born"), "{found:?}");
|
|
693
|
+
// Died before 1805 and born 1807: they cannot.
|
|
694
|
+
let found =
|
|
695
|
+
findings("0 HEAD\n0 @I1@ INDI\n1 BIRT\n2 DATE 1807\n1 DEAT\n2 DATE BEF 1805\n0 TRLR\n");
|
|
696
|
+
let died = found
|
|
697
|
+
.iter()
|
|
698
|
+
.find(|finding| finding.check == "died-before-born")
|
|
699
|
+
.expect("a contradiction");
|
|
700
|
+
assert_eq!(died.message, "born 1807 and died before 1805");
|
|
701
|
+
// A range is compared by both of its ends.
|
|
702
|
+
let found = findings(
|
|
703
|
+
"0 HEAD\n0 @I1@ INDI\n1 BIRT\n2 DATE BET 1800 AND 1810\n1 DEAT\n2 DATE 1805\n0 TRLR\n",
|
|
704
|
+
);
|
|
705
|
+
assert!(!checks(&found).contains(&"died-before-born"), "{found:?}");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
#[test]
|
|
709
|
+
fn a_bounded_marriage_says_its_age_once() {
|
|
710
|
+
let found = findings(
|
|
711
|
+
"0 HEAD\n\
|
|
712
|
+
0 @I1@ INDI\n\
|
|
713
|
+
1 BIRT\n\
|
|
714
|
+
2 DATE 1802\n\
|
|
715
|
+
0 @F1@ FAM\n\
|
|
716
|
+
1 HUSB @I1@\n\
|
|
717
|
+
1 MARR\n\
|
|
718
|
+
2 DATE BEF 1811\n\
|
|
719
|
+
0 TRLR\n",
|
|
720
|
+
);
|
|
721
|
+
let child = found
|
|
722
|
+
.iter()
|
|
723
|
+
.find(|finding| finding.check == "married-as-a-child")
|
|
724
|
+
.expect("married as a child");
|
|
725
|
+
assert_eq!(child.message, "married before 1811 at most 9, born 1802");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
#[test]
|
|
729
|
+
fn another_calendar_and_a_backwards_range_are_not_misread() {
|
|
730
|
+
// 5620 is a Hebrew year (1859-60), not a birth after a death in 1900.
|
|
731
|
+
let found = findings(
|
|
732
|
+
"0 HEAD\n\
|
|
733
|
+
0 @I1@ INDI\n\
|
|
734
|
+
1 BIRT\n\
|
|
735
|
+
2 DATE @#DHEBREW@ 5620\n\
|
|
736
|
+
1 DEAT\n\
|
|
737
|
+
2 DATE 1900\n\
|
|
738
|
+
0 TRLR\n",
|
|
739
|
+
);
|
|
740
|
+
assert!(!checks(&found).contains(&"died-before-born"), "{found:?}");
|
|
741
|
+
let found = findings(
|
|
742
|
+
"0 HEAD\n\
|
|
743
|
+
0 @I1@ INDI\n\
|
|
744
|
+
1 BIRT\n\
|
|
745
|
+
2 DATE BET 1910 AND 1900\n\
|
|
746
|
+
1 DEAT\n\
|
|
747
|
+
2 DATE 1905\n\
|
|
748
|
+
0 TRLR\n",
|
|
749
|
+
);
|
|
750
|
+
assert!(!checks(&found).contains(&"died-before-born"), "{found:?}");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
#[test]
|
|
754
|
+
fn a_child_born_before_its_parent_says_so_plainly() {
|
|
755
|
+
let found = findings(
|
|
756
|
+
"0 HEAD\n\
|
|
757
|
+
0 @I1@ INDI\n\
|
|
758
|
+
1 BIRT\n\
|
|
759
|
+
2 DATE BET 1850 AND 1855\n\
|
|
760
|
+
0 @I2@ INDI\n\
|
|
761
|
+
1 BIRT\n\
|
|
762
|
+
2 DATE BET 1840 AND 1845\n\
|
|
763
|
+
0 @F1@ FAM\n\
|
|
764
|
+
1 HUSB @I1@\n\
|
|
765
|
+
1 CHIL @I2@\n\
|
|
766
|
+
0 TRLR\n",
|
|
767
|
+
);
|
|
768
|
+
let young = found
|
|
769
|
+
.iter()
|
|
770
|
+
.find(|finding| finding.check == "parent-too-young")
|
|
771
|
+
.expect("born before the parent");
|
|
772
|
+
assert!(
|
|
773
|
+
young
|
|
774
|
+
.message
|
|
775
|
+
.ends_with("5 years before the parent was born"),
|
|
776
|
+
"{}",
|
|
777
|
+
young.message
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
#[test]
|
|
782
|
+
fn a_mother_dead_before_the_birth_needs_no_birth_year_of_her_own() {
|
|
783
|
+
let found = findings(
|
|
784
|
+
"0 HEAD\n\
|
|
785
|
+
0 @I1@ INDI\n\
|
|
786
|
+
1 DEAT\n\
|
|
787
|
+
2 DATE 1850\n\
|
|
788
|
+
0 @I2@ INDI\n\
|
|
789
|
+
1 BIRT\n\
|
|
790
|
+
2 DATE 1860\n\
|
|
791
|
+
0 @F1@ FAM\n\
|
|
792
|
+
1 WIFE @I1@\n\
|
|
793
|
+
1 CHIL @I2@\n\
|
|
794
|
+
0 TRLR\n",
|
|
795
|
+
);
|
|
796
|
+
assert!(
|
|
797
|
+
checks(&found).contains(&"child-born-after-mother-died"),
|
|
798
|
+
"{found:?}"
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
#[test]
|
|
803
|
+
fn a_parent_with_a_bounded_birth_is_too_young_only_when_every_age_is() {
|
|
804
|
+
let family = |parent: &str, child: &str| {
|
|
805
|
+
findings(&format!(
|
|
806
|
+
"0 HEAD\n0 @I1@ INDI\n1 BIRT\n2 DATE {parent}\n0 @I2@ INDI\n1 BIRT\n2 DATE {child}\n\
|
|
807
|
+
0 @F1@ FAM\n1 HUSB @I1@\n1 CHIL @I2@\n0 TRLR\n"
|
|
808
|
+
))
|
|
809
|
+
};
|
|
810
|
+
assert!(!checks(&family("AFT 1850", "1870")).contains(&"parent-too-young"));
|
|
811
|
+
let found = family("BET 1860 AND 1865", "1870");
|
|
812
|
+
let young = found
|
|
813
|
+
.iter()
|
|
814
|
+
.find(|finding| finding.check == "parent-too-young")
|
|
815
|
+
.expect("too young at every allowed age");
|
|
816
|
+
assert_eq!(
|
|
817
|
+
young.message,
|
|
818
|
+
"a parent born between 1860 and 1865 with a child born 1870, at most 10 years old"
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
552
822
|
#[test]
|
|
553
823
|
fn a_clean_record_produces_nothing_but_what_is_actually_missing() {
|
|
554
824
|
let found = findings(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|