gedcomkit 0.1.17__tar.gz → 0.1.18__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 (100) hide show
  1. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/CHANGELOG.md +126 -0
  2. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/Cargo.lock +1 -1
  3. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/Cargo.toml +1 -1
  4. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/PKG-INFO +1 -1
  5. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/README.md +3 -0
  6. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/bindings/gedcomkit-py/Cargo.lock +2 -2
  7. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/bindings/gedcomkit-py/Cargo.toml +1 -1
  8. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/bindings/gedcomkit-py/gedcomkit.pyi +5 -1
  9. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/bindings/gedcomkit-py/src/lib.rs +4 -1
  10. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/src/dates.rs +134 -7
  11. gedcomkit-0.1.18/src/validate.rs +3470 -0
  12. gedcomkit-0.1.17/src/validate.rs +0 -855
  13. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/LICENSE +0 -0
  14. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/bindings/gedcomkit-py/LICENSE +0 -0
  15. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/bindings/gedcomkit-py/README.md +0 -0
  16. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom-spec/LICENSE +0 -0
  17. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom-spec/README.md +0 -0
  18. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom-spec/cardinalities.tsv +0 -0
  19. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom-spec/enumerations.tsv +0 -0
  20. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom-spec/enumerationsets.tsv +0 -0
  21. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom-spec/grammar.abnf +0 -0
  22. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom-spec/grammar.gedstruct +0 -0
  23. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom-spec/payloads.tsv +0 -0
  24. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom-spec/substructures.tsv +0 -0
  25. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/age-invalid.ged +0 -0
  26. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/age-valid.ged +0 -0
  27. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/atsign.ged +0 -0
  28. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/char_ascii_1.ged +0 -0
  29. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/char_ascii_2.ged +0 -0
  30. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/char_utf16be-1.ged +0 -0
  31. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/char_utf16be-2.ged +0 -0
  32. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/char_utf16le-1.ged +0 -0
  33. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/char_utf16le-2.ged +0 -0
  34. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/char_utf8-1.ged +0 -0
  35. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/char_utf8-2.ged +0 -0
  36. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/char_utf8-3.ged +0 -0
  37. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/date-all.ged +0 -0
  38. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/date-dual-invalid.ged +0 -0
  39. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/date-dual-valid.ged +0 -0
  40. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/enum-ext.ged +0 -0
  41. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/filename-1.ged +0 -0
  42. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/lang-all.ged +0 -0
  43. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/notes-1.ged +0 -0
  44. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/obje-1.ged +0 -0
  45. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/obsolete-1.ged +0 -0
  46. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/pedi-1.ged +0 -0
  47. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/rela_1.ged +0 -0
  48. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/sour-1.ged +0 -0
  49. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/tiny-1.ged +0 -0
  50. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/5/xref-case.ged +0 -0
  51. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/age-invalid.ged +0 -0
  52. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/age-valid.ged +0 -0
  53. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/atsign.ged +0 -0
  54. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/char_ascii_1.ged +0 -0
  55. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/char_ascii_2.ged +0 -0
  56. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/char_utf16be-1.ged +0 -0
  57. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/char_utf16be-2.ged +0 -0
  58. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/char_utf16le-1.ged +0 -0
  59. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/char_utf16le-2.ged +0 -0
  60. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/char_utf8-1.ged +0 -0
  61. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/char_utf8-2.ged +0 -0
  62. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/char_utf8-3.ged +0 -0
  63. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/date-all.ged +0 -0
  64. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/date-dual-invalid.ged +0 -0
  65. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/enum-ext.ged +0 -0
  66. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/filename-1.ged +0 -0
  67. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/lang-all.ged +0 -0
  68. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/notes-1.ged +0 -0
  69. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/obje-1.ged +0 -0
  70. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/obsolete-1.ged +0 -0
  71. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/pedi-1.ged +0 -0
  72. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/rela_1.ged +0 -0
  73. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/sour-1.ged +0 -0
  74. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/tiny-1.ged +0 -0
  75. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/7/xref-case.ged +0 -0
  76. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/LICENSE +0 -0
  77. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/gedcom7code/UPSTREAM-README.md +0 -0
  78. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/zip-interop/README.md +0 -0
  79. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/zip-interop/python-zip64.gdz +0 -0
  80. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/fixtures/vendored/zip-interop/python-zipfile.gdz +0 -0
  81. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/pyproject.toml +0 -0
  82. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/src/build.rs +0 -0
  83. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/src/convert.rs +0 -0
  84. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/src/decode.rs +0 -0
  85. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/src/family.rs +0 -0
  86. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/src/gedzip.rs +0 -0
  87. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/src/lib.rs +0 -0
  88. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/src/names.rs +0 -0
  89. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/src/schema.rs +0 -0
  90. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/src/synthetic.rs +0 -0
  91. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/src/tags.rs +0 -0
  92. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/src/view.rs +0 -0
  93. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/tests/endurance.rs +0 -0
  94. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/tests/external_charset.rs +0 -0
  95. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/tests/properties.rs +0 -0
  96. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/tests/requirement_corpus.rs +0 -0
  97. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/tests/spec_conformance.rs +0 -0
  98. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/tests/typescript.rs +0 -0
  99. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/tests/vendored_corpus.rs +0 -0
  100. {gedcomkit-0.1.17 → gedcomkit-0.1.18}/tests/version_conversion.rs +0 -0
@@ -5,6 +5,132 @@ 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
+ ## [Unreleased]
9
+
10
+ ## [0.1.18] - 2026-10-16
11
+
12
+ ### Added
13
+
14
+ - `validate::inspect` runs nine more checks. Each has a stable name, and
15
+ the module documentation now lists every check with its severity:
16
+ - `married-after-died` (contradiction): a family's first `MARR` wholly
17
+ after a partner's death, or, failing a dated death, their burial. A
18
+ burial before the wedding beside a death after it is no finding here:
19
+ the death says when the life ended, and the burial is
20
+ `buried-before-died`'s.
21
+ - `child-born-long-after-father-died`: a child born more than 300 days
22
+ after the death of the family's `HUSB`. A contradiction when both dates
23
+ name a day; a question otherwise, since a bare year is often a guess.
24
+ - `christened-before-born` (contradiction): a `CHR`, `CHRA` or `BAPM`
25
+ wholly before the birth.
26
+ - `event-before-birth`: the mirror of `event-after-death`, for any other
27
+ event or attribute wholly before the birth year. A burial or cremation
28
+ is left to `died-before-born` and `buried-before-died` when the person
29
+ has a dated death.
30
+ - `buried-long-after-died` (question): a burial more than 365 days after
31
+ the death, counted from the latest day the death allows to the earliest
32
+ the burial does.
33
+ - `children-too-close`: two children of one mother born 2 to 270 days
34
+ apart. A contradiction when both dates name a day and are under 150
35
+ days apart; a question otherwise, since a very early birth makes the
36
+ longer gaps possible. A mother's children are taken from every family
37
+ she is the `WIFE` in; a family with no `WIFE` is compared alone.
38
+ - `possible-duplicate-child` (question): two children of one mother with
39
+ one given name (one name may be the start of the other: `Mira` and
40
+ `Mira Elowen`, but not `Mira Elowen` and `Mira Rose`), born on one
41
+ date, or on one day and month of different years.
42
+ - `conflicting-vitals` (question): two `BIRT`, or two `DEAT`, events of
43
+ one person that cannot both be true, even with an approximate date
44
+ allowed a year either way (`BIRT 1851` and `BIRT ABT 1850` are not in
45
+ conflict). GEDCOM lets a file record alternatives with the preferred
46
+ first, which is the one the other checks read, so this asks rather
47
+ than accuses.
48
+ - `own-ancestor` (contradiction): a loop in the parent links, read from
49
+ both directions of the family link as `FamilyGraph` reads them. One
50
+ finding per loop, on the person in it the file lists first, naming the
51
+ shortest way round: in full up to six steps, and past that its first
52
+ five and how many more, so a loop through 20,000 people is one short
53
+ message.
54
+
55
+ The three checks that compare pairs are bounded, so a hostile file cannot
56
+ make the report as large as the square of its people. `children-too-close`
57
+ and `possible-duplicate-child` report at most ten pairs for one mother,
58
+ and `conflicting-vitals` ten for one person's births and ten for their
59
+ deaths, the first in the order the file lists them; one more finding under
60
+ the same check, as serious as the most serious pair it stands for, then
61
+ says how many more pairs there are and how many in all (`4059990 more
62
+ pairs of children born 2 to 270 days apart, 4060000 in all`). The work is
63
+ bounded too: births and deaths are counted by sorting rather than compared
64
+ pair by pair; children who share a span of birth days, or a given name,
65
+ are compared as one group; and one mother's children are compared at most
66
+ 100,000 times for each check, past which the summary gives its counts as
67
+ lower bounds. A family of 3,000 children of one name born in one month,
68
+ which made 4.5 million sibling findings, makes 22; a person with 5,000
69
+ births, which made 12.5 million `conflicting-vitals` findings, makes 11.
70
+
71
+ ### Changed
72
+
73
+ - PyPI gets one wheel, Linux x86_64, beside the source distribution. The
74
+ macOS and Windows wheels are no longer cross-built; on those platforms pip
75
+ builds from the sdist, which needs a Rust toolchain.
76
+ - `died-before-born`, `buried-before-died`, `christened-before-born`,
77
+ `married-after-died` and `conflicting-vitals` compare as finely as both
78
+ dates allow: a burial on 12 September before a death on 14 September of
79
+ one year is now found. Julian and Gregorian dates are put on one count of days first.
80
+ An approximate date (`ABT`, `CAL`, `EST`) still vouches for its year and
81
+ nothing finer, and a bound (`BEF`, `AFT`, `BET`) admits its own day. A
82
+ finding the years alone decide keeps its wording (`born 1900 and died
83
+ 1880`); one only a finer comparison finds names the dates in full
84
+ (`died 14 September 1862 and buried 12 September 1862`).
85
+ - An approximate date (`ABT`, `CAL`, `EST`) may be a year out. A finding
86
+ that two events are out of order is a question, not a contradiction, when
87
+ moving each approximate date a year either way could put them in order:
88
+ `BIRT ABT 1850` with `CHR 1849` or `CENS 1849`, or `DEAT ABT 1850` with a
89
+ burial in 1849. Two years out it is still a contradiction. This holds for
90
+ `died-before-born`, `buried-before-died`, `event-after-death`,
91
+ `married-before-born` and `child-born-after-mother-died`, and for the new
92
+ `christened-before-born`, `event-before-birth` and `married-after-died`;
93
+ the checks about an age or an interval take an approximate year as
94
+ written. Their messages say an approximate date is one: `born about 1850,
95
+ and Christening is dated 1849`.
96
+ - `event-after-death` is a question, not a contradiction, for an `EVEN` or
97
+ `FACT`. Such an event is whatever its free-text `TYPE` says — a land
98
+ entry completed, a patent issued, an estate settled — and many of those
99
+ are done in a dead person's name. The other events keep their severity.
100
+ - `by` is read as a `BEF` qualifier: `by 5 October 1752` is
101
+ `BEF 5 OCT 1752`, where it was read as 5 May 1752. The qualifiers the
102
+ reader already knew (`abt`, `about`, `c.`, `ca.`, `bef`, `before`, `aft`,
103
+ `after`, and the others) are unchanged.
104
+
105
+ ### Fixed
106
+
107
+ - The date reader no longer shifts the pieces of a four-word date around a
108
+ word it does not know. `chr 10 Nov 1788` read as 10 October 1788,
109
+ `b 9 Jun 1750` as 9 September, `bapt. 3 May 1801` as 3 March: the leading
110
+ word was tried as a month, failed, and the day number was taken as the
111
+ month instead. Four pieces are now read only as a day, its ordinal ending
112
+ and a month written as a word, in either order (`Feb 11 th 1809`,
113
+ `11 th Feb 1809`, `1er janvier 1800`, `5te Januar 1800`); anything else is
114
+ `DateKind::Unparsed`, keeps its text as the phrase, and still offers its
115
+ year to sort by, so `to_gedcom_5_5_1` and `to_gedcom_7` return `None` and
116
+ the caller writes it as a date phrase. A month given only as a number never
117
+ decides the order of four pieces.
118
+ - A dual year (`10 JAN 1731/32`) admits both its years in every check, not
119
+ only where days are compared. The checks that compare years read it as
120
+ its first year alone, so `BIRT 1732` with `DEAT 10 JAN 1731/32` was
121
+ `died-before-born` ("born 1732 and died 1731"); it is now no finding, and
122
+ a death past both years says `died between 1731 and 1732`. Where the days
123
+ of two dates are compared, they alone decide the order, so a burial in the
124
+ Julian year 1700, which runs eleven days into the Gregorian 1701, is no
125
+ longer wholly before a death in the Gregorian 1701.
126
+ - `child-listed-twice` keeps the children it has seen in a set rather than
127
+ a list, so a family listing tens of thousands of children is checked in
128
+ time proportional to their number, not its square.
129
+ - The checks on an individual's own record read that record's dates when it
130
+ has no identifier, or shares one with another record. Before, such a
131
+ record was checked against no dates, or against those of the last record
132
+ with its identifier.
133
+
8
134
  ## [0.1.17] - 2026-10-16
9
135
 
10
136
  ### Changed
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.17"
38
+ version = "0.1.18"
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.17"
6
+ version = "0.1.18"
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.17
3
+ Version: 0.1.18
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -23,6 +23,9 @@ Python:
23
23
  python -m pip install gedcomkit
24
24
  ```
25
25
 
26
+ Wheels are published for Linux x86_64 only. Elsewhere pip builds the package from
27
+ source, which needs a Rust toolchain ([rustup](https://rustup.rs)).
28
+
26
29
  Node.js / WASM:
27
30
 
28
31
  ```bash
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.17"
38
+ version = "0.1.18"
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.17"
46
+ version = "0.1.18"
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.17"
5
+ version = "0.1.18"
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"
@@ -44,7 +44,11 @@ class Gedcom:
44
44
  def record(self, xref: str) -> dict[str, Any]: ...
45
45
  def individuals(self) -> list[dict[str, Any]]: ...
46
46
  def families(self) -> list[dict[str, Any]]: ...
47
- def validate(self) -> list[dict[str, Any]]: ...
47
+ def validate(self) -> list[dict[str, Any]]:
48
+ """Plausibility findings, each with ``record``, ``check``, ``severity``
49
+ and ``message``. ``check`` is the core's stable rule name, such as
50
+ ``died-before-born`` or ``own-ancestor``; the core's ``validate``
51
+ module documentation lists every one with its severity."""
48
52
  def unresolved_pointers(self) -> list[dict[str, Any]]: ...
49
53
  def parents_of(self, xref: str) -> list[str]: ...
50
54
  def children_of(self, xref: str) -> list[str]: ...
@@ -240,7 +240,10 @@ impl Gedcom {
240
240
  }
241
241
 
242
242
  /// Plausibility findings: contradictions, questions, and what cites
243
- /// nothing — reported with the numbers they were drawn from.
243
+ /// nothing — reported with the numbers they were drawn from. Each has
244
+ /// `record`, `check`, `severity` and `message`; `check` is the core's
245
+ /// stable rule name (`died-before-born`, `own-ancestor`, …), listed with
246
+ /// its severity in the core's `validate` module documentation.
244
247
  fn validate(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
245
248
  gedcomkit::validate::inspect(&self.document)
246
249
  .iter()
@@ -571,7 +571,7 @@ fn point_5_5_1(point: DatePoint) -> String {
571
571
  /// The first slashed year pair in a payload — `1740/41`, `1679/1683` — as
572
572
  /// two full years. `8/2/1664` is a numeric date, not a pair, and is skipped
573
573
  /// because a pair's first half has three or four digits.
574
- fn dual_year(text: &str) -> Option<(i32, i32)> {
574
+ pub(crate) fn dual_year(text: &str) -> Option<(i32, i32)> {
575
575
  let bytes = text.as_bytes();
576
576
  let mut index = 0;
577
577
  while index < bytes.len() {
@@ -721,9 +721,14 @@ const PREFIXES: &[(DateKind, &[&str])] = &[
721
721
  ),
722
722
  (DateKind::Calculated, &["CAL", "CALCULATED"]),
723
723
  (DateKind::Estimated, &["EST", "ESTIMATED"]),
724
+ // `by 5 October 1752` says the event had happened by that day. GEDCOM
725
+ // has no "on or before", and `BEF` is the nearest claim; `validate`
726
+ // compares a bound as admitting the bound itself, so nothing is lost.
724
727
  (
725
728
  DateKind::Before,
726
- &["BEF", "BEFORE", "AVANT", "VOR", "PRIMA", "ANTES", "FØR"],
729
+ &[
730
+ "BEF", "BEFORE", "BY", "AVANT", "VOR", "PRIMA", "ANTES", "FØR",
731
+ ],
727
732
  ),
728
733
  (
729
734
  DateKind::After,
@@ -930,20 +935,48 @@ fn parse_point(input: &str) -> Option<DatePoint> {
930
935
  })
931
936
  }
932
937
  [first, second, third, fourth] => {
933
- // `Feb 11 th 1809`, and `April 15 , 1873` once punctuation splits.
938
+ // Four pieces are a day with its ordinal ending beside a month
939
+ // written as a word: `Feb 11 th 1809` and `April 15th, 1873`
940
+ // month-first, `11 th Feb 1809` and `1er janvier 1800` day-first.
941
+ // Nothing else is read. A word the reader does not know in front
942
+ // of a date — `chr 10 Nov 1788`, `b 9 Jun 1750`, `bapt. 3 May
943
+ // 1801` — leaves the payload unparsed rather than shifting the
944
+ // pieces around it: guessing past such a word once took the day
945
+ // for the month and read `10 OCT 1788`.
934
946
  let year = parse_year(fourth)?;
935
- let month = month_number(first).or_else(|| month_number(second))?;
936
- let day = parse_day(second).or_else(|| parse_day(third))?;
947
+ let (month, day) = if is_month_name(first) && is_ordinal_ending(third) {
948
+ (first, second)
949
+ } else if is_month_name(third) && is_ordinal_ending(second) {
950
+ (third, first)
951
+ } else {
952
+ return None;
953
+ };
937
954
  Some(DatePoint {
938
955
  year,
939
- month: Some(month),
940
- day: Some(day),
956
+ month: Some(month_number(month)?),
957
+ day: Some(parse_day(day)?),
941
958
  })
942
959
  }
943
960
  _ => None,
944
961
  }
945
962
  }
946
963
 
964
+ /// Whether a token is a month written as a word, never as a number.
965
+ fn is_month_name(token: &str) -> bool {
966
+ !token.bytes().all(|byte| byte.is_ascii_digit()) && month_number(token).is_some()
967
+ }
968
+
969
+ /// Whether a token is the ending of an ordinal day once the digits have been
970
+ /// split from it: English `11th` and `2nd`, French `1er`, Danish and
971
+ /// Norwegian `1ste` and `5te`. (`22de` needs no entry: `de` is dropped as a
972
+ /// filler word before the pieces are counted.)
973
+ fn is_ordinal_ending(token: &str) -> bool {
974
+ matches!(
975
+ token.to_lowercase().as_str(),
976
+ "st" | "nd" | "rd" | "th" | "er" | "ste" | "te"
977
+ )
978
+ }
979
+
947
980
  fn parse_numeric(input: &str) -> Option<DatePoint> {
948
981
  for (separator, month_first) in [('.', false), ('/', true), ('-', false)] {
949
982
  let parts: Vec<&str> = input.split(separator).collect();
@@ -1538,6 +1571,100 @@ mod tests {
1538
1571
  }
1539
1572
  }
1540
1573
 
1574
+ #[test]
1575
+ fn a_word_in_front_of_a_date_never_turns_the_day_into_the_month() {
1576
+ // Each of these was once read with its day number as the month:
1577
+ // `chr 10 Nov 1788` as 10 October. A word the reader does not know
1578
+ // leaves the whole payload unparsed, so a writer keeps it as a phrase.
1579
+ for payload in [
1580
+ "chr 10 Nov 1788",
1581
+ "b 9 Jun 1750",
1582
+ "bapt. 3 May 1801",
1583
+ // Lower-case and full month names, and two-digit days.
1584
+ "chr 10 nov 1788",
1585
+ "chr 10 November 1788",
1586
+ "b 19 Jun 1750",
1587
+ "b. 19 june 1750",
1588
+ "bapt 3 MAY 1801",
1589
+ "Sunday 5 Jan 1882",
1590
+ // No month written as a word: nothing to anchor the order.
1591
+ "chr 10 11 1788",
1592
+ "chr Nov 1788",
1593
+ "b 1750",
1594
+ ] {
1595
+ let date = parsed(payload);
1596
+ assert_eq!(date.kind, DateKind::Unparsed, "{payload:?}");
1597
+ assert_eq!(date.earliest, None, "{payload:?}");
1598
+ assert_eq!(date.phrase.as_deref(), Some(payload), "{payload:?}");
1599
+ assert_eq!(date.to_gedcom_5_5_1(), None, "{payload:?}");
1600
+ assert_eq!(date.to_gedcom_7(), None, "{payload:?}");
1601
+ }
1602
+ // The year is still there to sort by.
1603
+ assert_eq!(parsed("chr 10 Nov 1788").year_hint(), Some(1788));
1604
+ }
1605
+
1606
+ #[test]
1607
+ fn a_known_qualifier_in_front_of_a_date_is_read_as_one() {
1608
+ let written = |text: &str| parsed(text).to_gedcom_5_5_1();
1609
+ let cases: &[(&str, &str)] = &[
1610
+ ("by 5 October 1752", "BEF 5 OCT 1752"),
1611
+ ("By 5 Oct 1752", "BEF 5 OCT 1752"),
1612
+ ("by 1752", "BEF 1752"),
1613
+ ("abt 10 Nov 1788", "ABT 10 NOV 1788"),
1614
+ ("abt. 10 nov 1788", "ABT 10 NOV 1788"),
1615
+ ("about 10 November 1788", "ABT 10 NOV 1788"),
1616
+ ("c. 3 May 1801", "ABT 3 MAY 1801"),
1617
+ ("ca. 3 May 1801", "ABT 3 MAY 1801"),
1618
+ ("ca 3 may 1801", "ABT 3 MAY 1801"),
1619
+ ("bef 19 Jun 1750", "BEF 19 JUN 1750"),
1620
+ ("bef. 19 Jun 1750", "BEF 19 JUN 1750"),
1621
+ ("before 19 June 1750", "BEF 19 JUN 1750"),
1622
+ ("aft 9 Jun 1750", "AFT 9 JUN 1750"),
1623
+ ("aft. 9 Jun 1750", "AFT 9 JUN 1750"),
1624
+ ("after 9 June 1750", "AFT 9 JUN 1750"),
1625
+ ];
1626
+ for (text, expected) in cases {
1627
+ assert_eq!(written(text).as_deref(), Some(*expected), "{text:?}");
1628
+ }
1629
+ // A word that merely begins like a qualifier is not one.
1630
+ assert_eq!(parsed("byre 5 Oct 1752").kind, DateKind::Unparsed);
1631
+ }
1632
+
1633
+ #[test]
1634
+ fn a_day_with_its_ordinal_ending_is_read_either_side_of_the_month() {
1635
+ type Case = (&'static str, i32, u8, u8);
1636
+ let cases: &[Case] = &[
1637
+ ("Feb 11 th 1809", 1809, 2, 11),
1638
+ ("Feb 11th 1809", 1809, 2, 11),
1639
+ ("April 15th, 1873", 1873, 4, 15),
1640
+ ("January 1st 1800", 1800, 1, 1),
1641
+ ("11 th Feb 1809", 1809, 2, 11),
1642
+ ("1st January 1800", 1800, 1, 1),
1643
+ ("23rd nov 1788", 1788, 11, 23),
1644
+ ("1er janvier 1800", 1800, 1, 1),
1645
+ ("5te Januar 1800", 1800, 1, 5),
1646
+ ("22de Juni 1848", 1848, 6, 22),
1647
+ ];
1648
+ for (payload, year, month, day) in cases {
1649
+ let date = parsed(payload);
1650
+ assert_eq!(date.kind, DateKind::Exact, "{payload:?}");
1651
+ assert_eq!(
1652
+ date.earliest,
1653
+ Some(DatePoint {
1654
+ year: *year,
1655
+ month: Some(*month),
1656
+ day: Some(*day)
1657
+ }),
1658
+ "{payload:?}"
1659
+ );
1660
+ }
1661
+ // A number where the month should be, or a word where the ending
1662
+ // should be, is not guessed around.
1663
+ for payload in ["11 15 th 1873", "Feb 11 foo 1809", "11 foo Feb 1809"] {
1664
+ assert_eq!(parsed(payload).kind, DateKind::Unparsed, "{payload:?}");
1665
+ }
1666
+ }
1667
+
1541
1668
  #[test]
1542
1669
  fn what_cannot_be_read_keeps_its_text_and_says_so() {
1543
1670
  for payload in ["Infant", "DEAD", "Unknown", "1799 or 12 April 1811"] {