gedcomkit 0.1.18__tar.gz → 0.1.19__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.18 → gedcomkit-0.1.19}/CHANGELOG.md +85 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/Cargo.lock +1 -1
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/Cargo.toml +1 -1
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/PKG-INFO +1 -1
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/bindings/gedcomkit-py/Cargo.lock +2 -2
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/bindings/gedcomkit-py/Cargo.toml +1 -1
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/bindings/gedcomkit-py/gedcomkit.pyi +27 -2
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/bindings/gedcomkit-py/src/lib.rs +3 -1
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/convert.rs +412 -7
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/dates.rs +440 -70
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/validate.rs +160 -122
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/view.rs +42 -1
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/tests/typescript.rs +1 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/LICENSE +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/README.md +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/bindings/gedcomkit-py/LICENSE +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/bindings/gedcomkit-py/README.md +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom-spec/LICENSE +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom-spec/README.md +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom-spec/cardinalities.tsv +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom-spec/enumerations.tsv +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom-spec/enumerationsets.tsv +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom-spec/grammar.abnf +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom-spec/grammar.gedstruct +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom-spec/payloads.tsv +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom-spec/substructures.tsv +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/age-invalid.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/age-valid.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/atsign.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/char_ascii_1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/char_ascii_2.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/char_utf16be-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/char_utf16be-2.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/char_utf16le-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/char_utf16le-2.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/char_utf8-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/char_utf8-2.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/char_utf8-3.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/date-all.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/date-dual-invalid.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/date-dual-valid.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/enum-ext.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/filename-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/lang-all.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/notes-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/obje-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/obsolete-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/pedi-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/rela_1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/sour-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/tiny-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/5/xref-case.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/age-invalid.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/age-valid.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/atsign.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/char_ascii_1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/char_ascii_2.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/char_utf16be-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/char_utf16be-2.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/char_utf16le-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/char_utf16le-2.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/char_utf8-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/char_utf8-2.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/char_utf8-3.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/date-all.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/date-dual-invalid.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/enum-ext.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/filename-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/lang-all.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/notes-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/obje-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/obsolete-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/pedi-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/rela_1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/sour-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/tiny-1.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/7/xref-case.ged +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/LICENSE +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/gedcom7code/UPSTREAM-README.md +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/zip-interop/README.md +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/zip-interop/python-zip64.gdz +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/fixtures/vendored/zip-interop/python-zipfile.gdz +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/pyproject.toml +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/build.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/decode.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/family.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/gedzip.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/lib.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/names.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/schema.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/synthetic.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/src/tags.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/tests/endurance.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/tests/external_charset.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/tests/properties.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/tests/requirement_corpus.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/tests/spec_conformance.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/tests/vendored_corpus.rs +0 -0
- {gedcomkit-0.1.18 → gedcomkit-0.1.19}/tests/version_conversion.rs +0 -0
|
@@ -7,6 +7,91 @@ rules.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.19] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `view::EventView::date_phrase` (`datePhrase` in the serialized view, so
|
|
15
|
+
in both bindings and the TypeScript definitions): the `PHRASE` beneath an
|
|
16
|
+
event's `DATE`, exactly as written. A GEDCOM 7 date with an empty payload
|
|
17
|
+
and a `PHRASE`, or a date with a phrase beside it, showed nothing of the
|
|
18
|
+
phrase before. A 5.5.1 phrase is written inside the payload, which `date`
|
|
19
|
+
already holds. The Python stub gains an `EventView` typed dict naming
|
|
20
|
+
every key of an event.
|
|
21
|
+
- The date reader reads ISO 8601's year and month: `1834-02` is February
|
|
22
|
+
1834, as `1834-02-10` was already 10 February 1834. Only four digits, a
|
|
23
|
+
hyphen and two digits are read, and only when the month is no more than
|
|
24
|
+
the year's last two digits: `1808-10`, `1711-12` and `2010-11` are as
|
|
25
|
+
often a span of years or a dual year as a month, and stay unparsed. So
|
|
26
|
+
does every month of a year ending in 00 (`1800-01` to `1800-12`), and
|
|
27
|
+
the later months of a year ending in 01 to 11 (`2005-06` to `2005-12`).
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- `validate::inspect` no longer reports two events out of order when an
|
|
32
|
+
approximate date's leeway could put them in order, which is how
|
|
33
|
+
`conflicting-vitals` already read them. A birth `ABT 1854` with a
|
|
34
|
+
christening in 1853 was a `christened-before-born` question; it is now
|
|
35
|
+
no finding. The same holds for `died-before-born`, `buried-before-died`,
|
|
36
|
+
`event-after-death`, `event-before-birth`, `married-before-born`,
|
|
37
|
+
`married-after-died` and `child-born-after-mother-died`, each of which
|
|
38
|
+
now reports only what is out of order beyond the leeway, as a
|
|
39
|
+
contradiction (a question for an `EVEN` or `FACT`, as before). Within the
|
|
40
|
+
leeway no other check fires in its place: a marriage in 1849 for somebody
|
|
41
|
+
born about 1850 is not `married-as-a-child`. Bounds (`BEF`, `AFT`, `BET`)
|
|
42
|
+
are read as the ranges they admit, unchanged.
|
|
43
|
+
- `convert::to_version_5_5_1` turns a name type back into 5.5.1's
|
|
44
|
+
spelling: `OTHER` with a one-line `PHRASE` becomes the phrase's words,
|
|
45
|
+
and an enumerated value is written in lower case (`BIRTH` becomes
|
|
46
|
+
`birth`). `OTHER` whose words are themselves an enumerated value (the
|
|
47
|
+
phrase `birth`) stays `other`, its `PHRASE` kept and reported, since the
|
|
48
|
+
words alone would convert back up as `BIRTH`. A `PHRASE` beside any other
|
|
49
|
+
value is kept and reported.
|
|
50
|
+
- `DatePoint::from_ymd` refuses a day its month never has (30 February,
|
|
51
|
+
31 April, day 0). The point is written with no calendar escape, which
|
|
52
|
+
makes it Gregorian, so 29 February is taken only in a Gregorian leap
|
|
53
|
+
year: 2000, not 1900.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- A range with an end the date reader cannot read is no longer read in
|
|
58
|
+
part. `FROM ABT 1924 TO 1959` was read as `TO 1959`, dropping the start
|
|
59
|
+
without a word, and `BET 1950 AND JULIAN DEC 302` as a range with only
|
|
60
|
+
its first end; `FROM Infant` was a period with neither end. Each is now
|
|
61
|
+
`DateKind::Unparsed`, keeps its text as the phrase, and still offers its
|
|
62
|
+
year to sort by; `to_version_7` keeps it whole as an empty `DATE` with a
|
|
63
|
+
`PHRASE`. A range whose ends are both dates reads as before.
|
|
64
|
+
- The date reader no longer accepts a day its month does not have.
|
|
65
|
+
`1834-02-30` and `30 FEB 1834` were read as 30 February 1834; they are
|
|
66
|
+
now unparsed, as is `29 FEB 1834`. Leap years follow the payload's
|
|
67
|
+
calendar: `@#DJULIAN@ 29 FEB 1900` is read and `@#DGREGORIAN@ 29 FEB
|
|
68
|
+
1900` is not. A payload with no calendar escape keeps the Julian leap
|
|
69
|
+
day, since files often leave the escape off a Julian date, so
|
|
70
|
+
`29 FEB 1900` is still read; ISO 8601's `1900-02-29` is not, since ISO
|
|
71
|
+
8601 is Gregorian by definition. A dual year's leap day is its second
|
|
72
|
+
year's, so `29 FEB 1711/12` and `@#DJULIAN@ 29 FEB 1711/12` are read. A
|
|
73
|
+
Roman or unrecognized calendar is held to the Julian month lengths, and
|
|
74
|
+
no Hebrew or French Republican month has more than 30 days. A numeric
|
|
75
|
+
date with a month or day out of range (`1834-13-05`, `13/05/1834`) is
|
|
76
|
+
unparsed too; before, the piece out of range was dropped and the rest
|
|
77
|
+
read (`1834-13-05` as the year 1834).
|
|
78
|
+
- `convert::to_version_5_5_1` no longer writes `INT (…)`, an interpreted
|
|
79
|
+
date with no date, for a version 7 `DATE` that is empty beside its
|
|
80
|
+
`PHRASE`; 5.5.1 does not allow it. The phrase is written as 5.5.1's date
|
|
81
|
+
phrase, `(FROM ABT 1924 TO 1959)`, or as the date itself when it is
|
|
82
|
+
already one in 5.5.1's grammar that version 7 cannot spell, so a dual
|
|
83
|
+
year (`1740/41`) comes back as the date it was.
|
|
84
|
+
- `convert::to_version_7` / `to_version_7_with` convert a personal name's
|
|
85
|
+
type (`INDI`.`NAME`.`TYPE`), which 5.5.1 writes as free text and version
|
|
86
|
+
7 as an enumeration. A value that is one of version 7's words in any case
|
|
87
|
+
(`aka`, `birth`, `immigrant`, `maiden`, `married`, `professional`,
|
|
88
|
+
`other`) becomes that word in upper case; any other becomes `OTHER`, with
|
|
89
|
+
the text kept verbatim as a `PHRASE`, the way a role has been converted
|
|
90
|
+
since 0.1.13. Before, `TYPE aka` was carried into the 7.0 document as it
|
|
91
|
+
stood, where it is not valid. A type that already has a `PHRASE`, is
|
|
92
|
+
empty, or has a continuation line with substructures is kept and
|
|
93
|
+
reported.
|
|
94
|
+
|
|
10
95
|
## [0.1.18] - 2026-10-16
|
|
11
96
|
|
|
12
97
|
### 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.19"
|
|
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.19"
|
|
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.19"
|
|
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.19"
|
|
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"
|
|
@@ -19,6 +19,29 @@ class RecordSpec(TypedDict):
|
|
|
19
19
|
value: NotRequired[str | None]
|
|
20
20
|
children: NotRequired[list[RecordSpec]]
|
|
21
21
|
|
|
22
|
+
class EventView(TypedDict):
|
|
23
|
+
"""One entry of an individual's or a family's ``events``.
|
|
24
|
+
|
|
25
|
+
``date`` is the DATE payload exactly as written. ``datePhrase`` is the
|
|
26
|
+
GEDCOM 7 PHRASE beneath that DATE, or ``None``: beside a date, or the
|
|
27
|
+
only text an empty 7.0 DATE has. A 5.5.1 phrase is part of ``date``.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
tag: str
|
|
31
|
+
label: str
|
|
32
|
+
date: str | None
|
|
33
|
+
datePhrase: str | None
|
|
34
|
+
dateDisplay: str | None
|
|
35
|
+
year: int | None
|
|
36
|
+
sortKey: int | None
|
|
37
|
+
place: str | None
|
|
38
|
+
age: str | None
|
|
39
|
+
cause: str | None
|
|
40
|
+
attribute: bool
|
|
41
|
+
value: str | None
|
|
42
|
+
citations: list[str]
|
|
43
|
+
path: list[int]
|
|
44
|
+
|
|
22
45
|
class WrappedLine(TypedDict):
|
|
23
46
|
record: str
|
|
24
47
|
path: list[int]
|
|
@@ -42,8 +65,10 @@ class Gedcom:
|
|
|
42
65
|
def content_records(self) -> int: ...
|
|
43
66
|
def records(self) -> list[dict[str, Any]]: ...
|
|
44
67
|
def record(self, xref: str) -> dict[str, Any]: ...
|
|
45
|
-
def individuals(self) -> list[dict[str, Any]]:
|
|
46
|
-
|
|
68
|
+
def individuals(self) -> list[dict[str, Any]]:
|
|
69
|
+
"""Every individual's view; each of its ``events`` is an ``EventView``."""
|
|
70
|
+
def families(self) -> list[dict[str, Any]]:
|
|
71
|
+
"""Every family's view; each of its ``events`` is an ``EventView``."""
|
|
47
72
|
def validate(self) -> list[dict[str, Any]]:
|
|
48
73
|
"""Plausibility findings, each with ``record``, ``check``, ``severity``
|
|
49
74
|
and ``message``. ``check`` is the core's stable rule name, such as
|
|
@@ -224,6 +224,8 @@ impl Gedcom {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
/// Every individual, read for display: names, events, families, IDs.
|
|
227
|
+
/// Each event has its `date` payload as written and, beside it,
|
|
228
|
+
/// `datePhrase`: the GEDCOM 7 `PHRASE` under the date, or `None`.
|
|
227
229
|
fn individuals(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
|
|
228
230
|
self.document
|
|
229
231
|
.records_of("INDI")
|
|
@@ -231,7 +233,7 @@ impl Gedcom {
|
|
|
231
233
|
.collect()
|
|
232
234
|
}
|
|
233
235
|
|
|
234
|
-
/// Every family record's view.
|
|
236
|
+
/// Every family record's view, its events shaped as `individuals`' are.
|
|
235
237
|
fn families(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
|
|
236
238
|
self.document
|
|
237
239
|
.records_of("FAM")
|
|
@@ -336,6 +336,16 @@ fn structures_to_7_node(node: &mut Node, shared: &[String], pass: &mut Pass) ->
|
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
// A personal name's type, free text in 5.5.1, is version 7's enumerated
|
|
340
|
+
// name type, the words as written kept in a `PHRASE` — as a role is.
|
|
341
|
+
if node.tag == "INDI" {
|
|
342
|
+
for name in node.children.iter_mut().filter(|child| child.tag == "NAME") {
|
|
343
|
+
for kind in name.children.iter_mut().filter(|child| child.tag == "TYPE") {
|
|
344
|
+
changed |= name_type_to_7(kind, pass);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
339
349
|
// A multimedia record's files: `FORM` moves from the object to each
|
|
340
350
|
// `FILE` (5.5), and each file's path, format and medium take version 7's
|
|
341
351
|
// shape (5.5.1 already nests `FORM` under `FILE`, with `TYPE` beneath).
|
|
@@ -977,6 +987,148 @@ fn role_to_7(role: &mut Node, pass: &mut Pass) -> bool {
|
|
|
977
987
|
true
|
|
978
988
|
}
|
|
979
989
|
|
|
990
|
+
/// Version 7's enumerated name types (`enumset-NAME-TYPE`).
|
|
991
|
+
const NAME_TYPES_7: &[&str] = &[
|
|
992
|
+
"AKA",
|
|
993
|
+
"BIRTH",
|
|
994
|
+
"IMMIGRANT",
|
|
995
|
+
"MAIDEN",
|
|
996
|
+
"MARRIED",
|
|
997
|
+
"PROFESSIONAL",
|
|
998
|
+
"OTHER",
|
|
999
|
+
];
|
|
1000
|
+
|
|
1001
|
+
/// A personal name's `TYPE` (`INDI`.`NAME`.`TYPE`) in version 7's shape.
|
|
1002
|
+
/// 5.5.1's values are free text, its own five (`aka`, `birth`, `immigrant`,
|
|
1003
|
+
/// `maiden`, `married`) written in lower case; version 7's are an upper-case
|
|
1004
|
+
/// enumeration. A value that is one of version 7's words in any case
|
|
1005
|
+
/// becomes that word; any other becomes `OTHER`, with the text kept verbatim
|
|
1006
|
+
/// as a `PHRASE`. A type that already has a `PHRASE`, is empty, or has a
|
|
1007
|
+
/// continuation line with substructures is kept and reported.
|
|
1008
|
+
fn name_type_to_7(kind: &mut Node, pass: &mut Pass) -> bool {
|
|
1009
|
+
const LABEL: &str = "NAME.TYPE";
|
|
1010
|
+
let original = kind.logical_value();
|
|
1011
|
+
let trimmed = original.trim();
|
|
1012
|
+
let upper = trimmed.to_ascii_uppercase();
|
|
1013
|
+
if NAME_TYPES_7.contains(&upper.as_str()) && upper == original {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
if continuation_has_children(kind) {
|
|
1017
|
+
refuse_for_continuation_children(LABEL, "name type", pass);
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
if NAME_TYPES_7.contains(&upper.as_str()) {
|
|
1021
|
+
kind.set_logical_value(&upper);
|
|
1022
|
+
pass.note(
|
|
1023
|
+
LABEL,
|
|
1024
|
+
ConversionKind::Converted,
|
|
1025
|
+
format!("the name type {original:?} is version 7's enumerated value {upper}"),
|
|
1026
|
+
);
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
if trimmed.is_empty() {
|
|
1030
|
+
pass.note(
|
|
1031
|
+
LABEL,
|
|
1032
|
+
ConversionKind::Kept,
|
|
1033
|
+
"an empty name type has no version 7 reading; it is left as it is",
|
|
1034
|
+
);
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
if kind.first("PHRASE").is_some() {
|
|
1038
|
+
pass.note(
|
|
1039
|
+
LABEL,
|
|
1040
|
+
ConversionKind::Kept,
|
|
1041
|
+
format!(
|
|
1042
|
+
"the name type {original:?} is not version 7's enumeration, but it already has a PHRASE to keep the words in; it is left as it is"
|
|
1043
|
+
),
|
|
1044
|
+
);
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
kind.set_logical_value("OTHER");
|
|
1048
|
+
// The text moves as written, as a role's does: a leading `@` that 5.5.1
|
|
1049
|
+
// escaped as `@@` is version 7's escape too.
|
|
1050
|
+
kind.push(Node::with_value("PHRASE", trimmed));
|
|
1051
|
+
pass.note(
|
|
1052
|
+
LABEL,
|
|
1053
|
+
ConversionKind::Converted,
|
|
1054
|
+
format!(
|
|
1055
|
+
"the name type {original:?} is none of version 7's enumerated values; it became OTHER, with the words kept as a PHRASE"
|
|
1056
|
+
),
|
|
1057
|
+
);
|
|
1058
|
+
true
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/// A version 7 name type in 5.5.1's spelling. `OTHER` with a one-line
|
|
1062
|
+
/// `PHRASE` becomes the phrase's words, which is how 5.5.1 holds a type it
|
|
1063
|
+
/// does not list — unless the words are themselves one of the enumerated
|
|
1064
|
+
/// values (`OTHER` with the phrase `birth`), which would come back as that
|
|
1065
|
+
/// value, not as `OTHER`. Any other enumerated value, and `OTHER` then, is
|
|
1066
|
+
/// written in lower case, as 5.5.1 writes its own five; a `PHRASE` beside
|
|
1067
|
+
/// one of those, or one that spans lines or has substructures, has no place
|
|
1068
|
+
/// in 5.5.1 and is kept as written and reported.
|
|
1069
|
+
fn name_type_to_5(kind: &mut Node, level: usize, pass: &mut Pass) -> bool {
|
|
1070
|
+
const LABEL: &str = "NAME.TYPE";
|
|
1071
|
+
let original = kind.logical_value();
|
|
1072
|
+
let enumerated = original.trim().to_ascii_uppercase();
|
|
1073
|
+
if !NAME_TYPES_7.contains(&enumerated.as_str()) {
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
if continuation_has_children(kind) {
|
|
1077
|
+
refuse_for_continuation_children(LABEL, "name type", pass);
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
let phrase = kind
|
|
1081
|
+
.first("PHRASE")
|
|
1082
|
+
.filter(|phrase| phrase.substructures().next().is_none())
|
|
1083
|
+
.map(Node::logical_value)
|
|
1084
|
+
.filter(|phrase| !phrase.contains('\n') && !phrase.trim().is_empty());
|
|
1085
|
+
let reads_as_enumerated = phrase
|
|
1086
|
+
.as_deref()
|
|
1087
|
+
.is_some_and(|words| NAME_TYPES_7.contains(&words.trim().to_ascii_uppercase().as_str()));
|
|
1088
|
+
let changed = match phrase {
|
|
1089
|
+
Some(words)
|
|
1090
|
+
if enumerated == "OTHER" && kind.all("PHRASE").count() == 1 && !reads_as_enumerated =>
|
|
1091
|
+
{
|
|
1092
|
+
remove_child(kind, "PHRASE");
|
|
1093
|
+
kind.set_logical_value(words.trim());
|
|
1094
|
+
pass.note(
|
|
1095
|
+
LABEL,
|
|
1096
|
+
ConversionKind::Converted,
|
|
1097
|
+
"version 7's OTHER name type with a PHRASE is 5.5.1's name type in words",
|
|
1098
|
+
);
|
|
1099
|
+
true
|
|
1100
|
+
}
|
|
1101
|
+
_ => {
|
|
1102
|
+
let lower = enumerated.to_ascii_lowercase();
|
|
1103
|
+
let changed = lower != original;
|
|
1104
|
+
if changed {
|
|
1105
|
+
kind.set_logical_value(&lower);
|
|
1106
|
+
pass.note(
|
|
1107
|
+
LABEL,
|
|
1108
|
+
ConversionKind::Converted,
|
|
1109
|
+
format!("the name type {original:?} is {lower:?} in 5.5.1's spelling"),
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
if kind.first("PHRASE").is_some() {
|
|
1113
|
+
pass.note(
|
|
1114
|
+
"PHRASE",
|
|
1115
|
+
ConversionKind::Kept,
|
|
1116
|
+
if enumerated == "OTHER" && reads_as_enumerated {
|
|
1117
|
+
"5.5.1 has no phrase beside a name type, and this OTHER type's words are themselves an enumerated value, which they would be read as; the PHRASE is kept as written, which 5.5.1 does not define"
|
|
1118
|
+
} else {
|
|
1119
|
+
"5.5.1 has no phrase beside a name type; the PHRASE is kept as written, which 5.5.1 does not define"
|
|
1120
|
+
},
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
changed
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
if changed {
|
|
1127
|
+
crate::build::fit_own_lines(kind, level, crate::build::MAX_LINE_LENGTH);
|
|
1128
|
+
}
|
|
1129
|
+
changed
|
|
1130
|
+
}
|
|
1131
|
+
|
|
980
1132
|
/// The roles 5.5.1 enumerates under a cited event.
|
|
981
1133
|
const ROLES_5: &[&str] = &["CHIL", "HUSB", "WIFE", "MOTH", "FATH", "SPOU"];
|
|
982
1134
|
|
|
@@ -1204,7 +1356,10 @@ fn structures_to_5_node(node: &mut Node, level: usize, pass: &mut Pass) -> bool
|
|
|
1204
1356
|
}
|
|
1205
1357
|
|
|
1206
1358
|
// `DATE` with a `PHRASE` folds back into 5.5.1's interpreted form, which
|
|
1207
|
-
// is the same information in the older spelling.
|
|
1359
|
+
// is the same information in the older spelling. An empty date has no
|
|
1360
|
+
// `INT` form, which needs a date: its phrase is 5.5.1's date phrase
|
|
1361
|
+
// `(…)`, or the date itself when the words are already a date in 5.5.1's
|
|
1362
|
+
// grammar that version 7 cannot spell, such as a dual year.
|
|
1208
1363
|
if node.tag == "DATE"
|
|
1209
1364
|
&& let Some(phrase_node) = node.first("PHRASE")
|
|
1210
1365
|
{
|
|
@@ -1226,15 +1381,28 @@ fn structures_to_5_node(node: &mut Node, level: usize, pass: &mut Pass) -> bool
|
|
|
1226
1381
|
"5.5.1's interpreted date holds one line and nothing beneath it, and this date or phrase spans lines or has substructures; the PHRASE is kept as written, which 5.5.1 does not define",
|
|
1227
1382
|
);
|
|
1228
1383
|
} else {
|
|
1384
|
+
let words = phrase.trim();
|
|
1385
|
+
let (written, detail) = if !payload.trim().is_empty() {
|
|
1386
|
+
(
|
|
1387
|
+
format!("INT {payload} ({phrase})"),
|
|
1388
|
+
"a dated phrase became 5.5.1's interpreted form",
|
|
1389
|
+
)
|
|
1390
|
+
} else if GedcomDate::parse(words).to_gedcom_5_5_1().as_deref() == Some(words) {
|
|
1391
|
+
(
|
|
1392
|
+
words.to_owned(),
|
|
1393
|
+
"a phrase with no date beside it is a date in 5.5.1's own grammar, which version 7 cannot spell; it became the date",
|
|
1394
|
+
)
|
|
1395
|
+
} else {
|
|
1396
|
+
(
|
|
1397
|
+
format!("({words})"),
|
|
1398
|
+
"a phrase with no date beside it became 5.5.1's date phrase",
|
|
1399
|
+
)
|
|
1400
|
+
};
|
|
1229
1401
|
remove_child(node, "PHRASE");
|
|
1230
|
-
node.set_logical_value(&
|
|
1402
|
+
node.set_logical_value(&written);
|
|
1231
1403
|
changed = true;
|
|
1232
1404
|
own = true;
|
|
1233
|
-
pass.note(
|
|
1234
|
-
"PHRASE",
|
|
1235
|
-
ConversionKind::Converted,
|
|
1236
|
-
"a dated phrase became 5.5.1's interpreted form",
|
|
1237
|
-
);
|
|
1405
|
+
pass.note("PHRASE", ConversionKind::Converted, detail);
|
|
1238
1406
|
}
|
|
1239
1407
|
}
|
|
1240
1408
|
|
|
@@ -1250,6 +1418,15 @@ fn structures_to_5_node(node: &mut Node, level: usize, pass: &mut Pass) -> bool
|
|
|
1250
1418
|
}
|
|
1251
1419
|
}
|
|
1252
1420
|
|
|
1421
|
+
// Version 7's enumerated name type goes back to 5.5.1's free text.
|
|
1422
|
+
if node.tag == "INDI" {
|
|
1423
|
+
for name in node.children.iter_mut().filter(|child| child.tag == "NAME") {
|
|
1424
|
+
for kind in name.children.iter_mut().filter(|child| child.tag == "TYPE") {
|
|
1425
|
+
changed |= name_type_to_5(kind, level + 2, pass);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1253
1430
|
// Everything else version 7 added and 5.5.1 does not define is kept and
|
|
1254
1431
|
// named. Deleting a translation or a creation date to satisfy a version
|
|
1255
1432
|
// number would be the worst trade this application could make.
|
|
@@ -1478,6 +1655,8 @@ mod tests {
|
|
|
1478
1655
|
("ABT Oct 1803", "ABT OCT 1803"),
|
|
1479
1656
|
("44 B.C.", "44 BCE"),
|
|
1480
1657
|
("@#DJULIAN@ 10 MAR 1700", "JULIAN 10 MAR 1700"),
|
|
1658
|
+
("1834-02", "FEB 1834"),
|
|
1659
|
+
("1834-02-10", "10 FEB 1834"),
|
|
1481
1660
|
];
|
|
1482
1661
|
for (original, rendered) in cases {
|
|
1483
1662
|
let document = parse(&format!(
|
|
@@ -1504,6 +1683,232 @@ mod tests {
|
|
|
1504
1683
|
}
|
|
1505
1684
|
}
|
|
1506
1685
|
|
|
1686
|
+
#[test]
|
|
1687
|
+
fn a_date_the_reader_once_read_in_part_is_kept_whole_as_a_phrase() {
|
|
1688
|
+
// `FROM ABT 1924 TO 1959` was written as `TO 1959`, and `30 FEB 1834`
|
|
1689
|
+
// passed through as a date: neither is version 7's grammar, and the
|
|
1690
|
+
// first lost its start.
|
|
1691
|
+
for original in [
|
|
1692
|
+
"FROM ABT 1924 TO 1959",
|
|
1693
|
+
"BET ABT 1900 AND 1910",
|
|
1694
|
+
"30 FEB 1834",
|
|
1695
|
+
"1834-02-30",
|
|
1696
|
+
] {
|
|
1697
|
+
let document = parse(&format!(
|
|
1698
|
+
"0 HEAD\n1 GEDC\n2 VERS 5.5.1\n0 @I1@ INDI\n1 RESI\n2 DATE {original}\n0 TRLR\n"
|
|
1699
|
+
));
|
|
1700
|
+
let outcome = to_version_7(&document);
|
|
1701
|
+
let text = outcome.document.to_text();
|
|
1702
|
+
assert!(
|
|
1703
|
+
text.contains(&format!("2 DATE\n3 PHRASE {original}\n")),
|
|
1704
|
+
"{original:?} -> {text}"
|
|
1705
|
+
);
|
|
1706
|
+
let note = outcome
|
|
1707
|
+
.notes
|
|
1708
|
+
.iter()
|
|
1709
|
+
.find(|note| note.tag == "DATE")
|
|
1710
|
+
.unwrap_or_else(|| panic!("a note about {original:?}"));
|
|
1711
|
+
assert!(note.detail.contains("cannot hold"), "{}", note.detail);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
#[test]
|
|
1716
|
+
fn a_name_type_in_words_becomes_version_7s_enumeration_with_its_phrase() {
|
|
1717
|
+
let document = parse(
|
|
1718
|
+
"0 HEAD\n1 GEDC\n2 VERS 5.5.1\n0 @I1@ INDI\n\
|
|
1719
|
+
1 NAME Wren /Testwright/\n2 TYPE aka\n\
|
|
1720
|
+
1 NAME Wren /Elder/\n2 TYPE Birth\n\
|
|
1721
|
+
1 NAME Wren /Stage/\n2 TYPE professional\n\
|
|
1722
|
+
1 NAME Wren /Quill/\n2 TYPE MARRIED\n\
|
|
1723
|
+
1 NAME Wren /Nick/\n2 TYPE Nickname\n\
|
|
1724
|
+
1 NAME Wren /Shelf/\n2 TYPE also known as\n\
|
|
1725
|
+
0 @R1@ REPO\n1 NAME Example Archive\n2 TYPE county office\n0 TRLR\n",
|
|
1726
|
+
);
|
|
1727
|
+
let outcome = to_version_7(&document);
|
|
1728
|
+
let text = outcome.document.to_text();
|
|
1729
|
+
for expected in [
|
|
1730
|
+
"1 NAME Wren /Testwright/\n2 TYPE AKA\n1 NAME",
|
|
1731
|
+
"1 NAME Wren /Elder/\n2 TYPE BIRTH\n1 NAME",
|
|
1732
|
+
"1 NAME Wren /Stage/\n2 TYPE PROFESSIONAL\n1 NAME",
|
|
1733
|
+
"1 NAME Wren /Quill/\n2 TYPE MARRIED\n1 NAME",
|
|
1734
|
+
"1 NAME Wren /Nick/\n2 TYPE OTHER\n3 PHRASE Nickname\n1 NAME",
|
|
1735
|
+
"1 NAME Wren /Shelf/\n2 TYPE OTHER\n3 PHRASE also known as\n0 @R1@",
|
|
1736
|
+
// Only a person's name has a type in either version.
|
|
1737
|
+
"1 NAME Example Archive\n2 TYPE county office\n",
|
|
1738
|
+
] {
|
|
1739
|
+
assert!(text.contains(expected), "{expected:?} in {text}");
|
|
1740
|
+
}
|
|
1741
|
+
let notes: Vec<&ConversionNote> = outcome
|
|
1742
|
+
.notes
|
|
1743
|
+
.iter()
|
|
1744
|
+
.filter(|note| note.tag == "NAME.TYPE")
|
|
1745
|
+
.collect();
|
|
1746
|
+
assert_eq!(
|
|
1747
|
+
notes.len(),
|
|
1748
|
+
5,
|
|
1749
|
+
"an enumerated value is untouched: {notes:?}"
|
|
1750
|
+
);
|
|
1751
|
+
assert!(
|
|
1752
|
+
notes
|
|
1753
|
+
.iter()
|
|
1754
|
+
.all(|note| note.kind == ConversionKind::Converted)
|
|
1755
|
+
);
|
|
1756
|
+
assert!(
|
|
1757
|
+
notes
|
|
1758
|
+
.iter()
|
|
1759
|
+
.any(|note| note.detail.contains("\"Nickname\"")),
|
|
1760
|
+
"{notes:?}"
|
|
1761
|
+
);
|
|
1762
|
+
// A second conversion has nothing left to do.
|
|
1763
|
+
assert_eq!(to_version_7(&outcome.document).document.to_text(), text);
|
|
1764
|
+
|
|
1765
|
+
// And back: the words return as 5.5.1's free text, and the
|
|
1766
|
+
// enumerated values in 5.5.1's lower case.
|
|
1767
|
+
let back = to_version_5_5_1(&outcome.document);
|
|
1768
|
+
let back_text = back.document.to_text();
|
|
1769
|
+
for expected in [
|
|
1770
|
+
"2 TYPE aka\n",
|
|
1771
|
+
"2 TYPE birth\n",
|
|
1772
|
+
"2 TYPE professional\n",
|
|
1773
|
+
"2 TYPE married\n",
|
|
1774
|
+
"2 TYPE Nickname\n",
|
|
1775
|
+
"2 TYPE also known as\n",
|
|
1776
|
+
"2 TYPE county office\n",
|
|
1777
|
+
] {
|
|
1778
|
+
assert!(back_text.contains(expected), "{expected:?} in {back_text}");
|
|
1779
|
+
}
|
|
1780
|
+
assert!(!back_text.contains("PHRASE"), "{back_text}");
|
|
1781
|
+
assert!(
|
|
1782
|
+
back.notes
|
|
1783
|
+
.iter()
|
|
1784
|
+
.all(|note| note.kind != ConversionKind::Kept),
|
|
1785
|
+
"{:?}",
|
|
1786
|
+
back.notes
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
#[test]
|
|
1791
|
+
fn a_name_type_that_cannot_be_converted_is_kept_and_reported() {
|
|
1792
|
+
let document = parse(
|
|
1793
|
+
"0 HEAD\n1 GEDC\n2 VERS 5.5.1\n0 @I1@ INDI\n\
|
|
1794
|
+
1 NAME Wren /Testwright/\n2 TYPE Nickname\n3 PHRASE already here\n\
|
|
1795
|
+
1 NAME Wren /Blank/\n2 TYPE\n0 TRLR\n",
|
|
1796
|
+
);
|
|
1797
|
+
let outcome = to_version_7(&document);
|
|
1798
|
+
let text = outcome.document.to_text();
|
|
1799
|
+
assert!(
|
|
1800
|
+
text.contains("2 TYPE Nickname\n3 PHRASE already here\n"),
|
|
1801
|
+
"{text}"
|
|
1802
|
+
);
|
|
1803
|
+
assert!(text.contains("1 NAME Wren /Blank/\n2 TYPE\n"), "{text}");
|
|
1804
|
+
let kept: Vec<&ConversionNote> = outcome
|
|
1805
|
+
.notes
|
|
1806
|
+
.iter()
|
|
1807
|
+
.filter(|note| note.tag == "NAME.TYPE")
|
|
1808
|
+
.collect();
|
|
1809
|
+
assert_eq!(kept.len(), 2, "{kept:?}");
|
|
1810
|
+
assert!(kept.iter().all(|note| note.kind == ConversionKind::Kept));
|
|
1811
|
+
|
|
1812
|
+
// Going down, an enumerated value with a phrase beside it keeps the
|
|
1813
|
+
// phrase rather than losing either.
|
|
1814
|
+
let seven = parse(
|
|
1815
|
+
"0 HEAD\n1 GEDC\n2 VERS 7.0\n0 @I1@ INDI\n1 NAME Wren /Testwright/\n2 TYPE BIRTH\n3 PHRASE name at baptism\n0 TRLR\n",
|
|
1816
|
+
);
|
|
1817
|
+
let back = to_version_5_5_1(&seven);
|
|
1818
|
+
let back_text = back.document.to_text();
|
|
1819
|
+
assert!(
|
|
1820
|
+
back_text.contains("2 TYPE birth\n3 PHRASE name at baptism\n"),
|
|
1821
|
+
"{back_text}"
|
|
1822
|
+
);
|
|
1823
|
+
assert!(
|
|
1824
|
+
back.notes
|
|
1825
|
+
.iter()
|
|
1826
|
+
.any(|note| note.tag == "PHRASE" && note.kind == ConversionKind::Kept),
|
|
1827
|
+
"{:?}",
|
|
1828
|
+
back.notes
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
#[test]
|
|
1833
|
+
fn a_name_type_round_trips_without_growing_or_changing_its_meaning() {
|
|
1834
|
+
// A leading `@` is escaped as `@@` in both versions, so it moves as
|
|
1835
|
+
// written; it once gained an `@` on every conversion up.
|
|
1836
|
+
let five = parse(
|
|
1837
|
+
"0 HEAD\n1 GEDC\n2 VERS 5.5.1\n0 @I1@ INDI\n1 NAME Wren /Testwright/\n2 TYPE @@home\n0 TRLR\n",
|
|
1838
|
+
);
|
|
1839
|
+
let seven = to_version_7(&five).document;
|
|
1840
|
+
let text = seven.to_text();
|
|
1841
|
+
assert!(text.contains("2 TYPE OTHER\n3 PHRASE @@home\n"), "{text}");
|
|
1842
|
+
let back = to_version_5_5_1(&seven).document;
|
|
1843
|
+
let back_text = back.to_text();
|
|
1844
|
+
assert!(back_text.contains("2 TYPE @@home\n"), "{back_text}");
|
|
1845
|
+
assert_eq!(to_version_7(&back).document.to_text(), text);
|
|
1846
|
+
|
|
1847
|
+
// `OTHER` whose words are an enumerated value would come back up as
|
|
1848
|
+
// that value, so it stays `other` beside its phrase.
|
|
1849
|
+
let seven = parse(
|
|
1850
|
+
"0 HEAD\n1 GEDC\n2 VERS 7.0\n0 @I1@ INDI\n1 NAME Wren /Testwright/\n2 TYPE OTHER\n3 PHRASE birth\n0 TRLR\n",
|
|
1851
|
+
);
|
|
1852
|
+
let down = to_version_5_5_1(&seven);
|
|
1853
|
+
let down_text = down.document.to_text();
|
|
1854
|
+
assert!(
|
|
1855
|
+
down_text.contains("2 TYPE other\n3 PHRASE birth\n"),
|
|
1856
|
+
"{down_text}"
|
|
1857
|
+
);
|
|
1858
|
+
assert!(
|
|
1859
|
+
down.notes
|
|
1860
|
+
.iter()
|
|
1861
|
+
.any(|note| note.tag == "PHRASE" && note.kind == ConversionKind::Kept),
|
|
1862
|
+
"{:?}",
|
|
1863
|
+
down.notes
|
|
1864
|
+
);
|
|
1865
|
+
let up = to_version_7(&down.document).document.to_text();
|
|
1866
|
+
assert!(up.contains("2 TYPE OTHER\n3 PHRASE birth\n"), "{up}");
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
#[test]
|
|
1870
|
+
fn an_empty_date_with_a_phrase_goes_down_as_a_valid_5_5_1_date() {
|
|
1871
|
+
// `INT` needs a date beside its phrase, so an empty one was written
|
|
1872
|
+
// `INT (…)`, which 5.5.1 does not allow.
|
|
1873
|
+
for (original, down) in [
|
|
1874
|
+
("FROM ABT 1924 TO 1959", "(FROM ABT 1924 TO 1959)"),
|
|
1875
|
+
("Infant", "(Infant)"),
|
|
1876
|
+
// A dual year is a 5.5.1 date that version 7 cannot spell.
|
|
1877
|
+
("1740/41", "1740/41"),
|
|
1878
|
+
("10 JAN 1699/00", "10 JAN 1699/00"),
|
|
1879
|
+
] {
|
|
1880
|
+
let five = parse(&format!(
|
|
1881
|
+
"0 HEAD\n1 GEDC\n2 VERS 5.5.1\n0 @I1@ INDI\n1 BIRT\n2 DATE {original}\n0 TRLR\n"
|
|
1882
|
+
));
|
|
1883
|
+
let seven = to_version_7(&five).document;
|
|
1884
|
+
assert!(
|
|
1885
|
+
seven
|
|
1886
|
+
.to_text()
|
|
1887
|
+
.contains(&format!("2 DATE\n3 PHRASE {original}\n")),
|
|
1888
|
+
"{original:?}"
|
|
1889
|
+
);
|
|
1890
|
+
let outcome = to_version_5_5_1(&seven);
|
|
1891
|
+
let text = outcome.document.to_text();
|
|
1892
|
+
assert!(
|
|
1893
|
+
text.contains(&format!("1 BIRT\n2 DATE {down}\n")),
|
|
1894
|
+
"{original:?} -> {text}"
|
|
1895
|
+
);
|
|
1896
|
+
assert!(!text.contains("INT") && !text.contains("PHRASE"), "{text}");
|
|
1897
|
+
let note = outcome
|
|
1898
|
+
.notes
|
|
1899
|
+
.iter()
|
|
1900
|
+
.find(|note| note.tag == "PHRASE")
|
|
1901
|
+
.expect("a note about the phrase");
|
|
1902
|
+
assert_eq!(note.kind, ConversionKind::Converted);
|
|
1903
|
+
}
|
|
1904
|
+
// A version 7 phrase of its own goes down the same way.
|
|
1905
|
+
let seven = parse(
|
|
1906
|
+
"0 HEAD\n1 GEDC\n2 VERS 7.0\n0 @I1@ INDI\n1 BIRT\n2 DATE\n3 PHRASE in his youth\n0 TRLR\n",
|
|
1907
|
+
);
|
|
1908
|
+
let text = to_version_5_5_1(&seven).document.to_text();
|
|
1909
|
+
assert!(text.contains("2 DATE (in his youth)\n"), "{text}");
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1507
1912
|
#[test]
|
|
1508
1913
|
fn a_dual_year_has_no_version_7_reading_and_becomes_an_empty_date_with_a_phrase() {
|
|
1509
1914
|
let document =
|