gedcomkit 0.1.19__tar.gz → 0.1.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/CHANGELOG.md +12 -0
  2. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/Cargo.lock +1 -1
  3. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/Cargo.toml +1 -1
  4. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/PKG-INFO +1 -1
  5. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/README.md +2 -6
  6. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/bindings/gedcomkit-py/Cargo.lock +2 -2
  7. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/bindings/gedcomkit-py/Cargo.toml +1 -1
  8. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/build.rs +1 -1
  9. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/dates.rs +139 -3
  10. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/LICENSE +0 -0
  11. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/bindings/gedcomkit-py/LICENSE +0 -0
  12. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/bindings/gedcomkit-py/README.md +0 -0
  13. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/bindings/gedcomkit-py/gedcomkit.pyi +0 -0
  14. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/bindings/gedcomkit-py/src/lib.rs +0 -0
  15. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom-spec/LICENSE +0 -0
  16. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom-spec/README.md +0 -0
  17. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom-spec/cardinalities.tsv +0 -0
  18. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom-spec/enumerations.tsv +0 -0
  19. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom-spec/enumerationsets.tsv +0 -0
  20. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom-spec/grammar.abnf +0 -0
  21. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom-spec/grammar.gedstruct +0 -0
  22. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom-spec/payloads.tsv +0 -0
  23. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom-spec/substructures.tsv +0 -0
  24. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/age-invalid.ged +0 -0
  25. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/age-valid.ged +0 -0
  26. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/atsign.ged +0 -0
  27. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/char_ascii_1.ged +0 -0
  28. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/char_ascii_2.ged +0 -0
  29. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/char_utf16be-1.ged +0 -0
  30. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/char_utf16be-2.ged +0 -0
  31. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/char_utf16le-1.ged +0 -0
  32. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/char_utf16le-2.ged +0 -0
  33. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/char_utf8-1.ged +0 -0
  34. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/char_utf8-2.ged +0 -0
  35. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/char_utf8-3.ged +0 -0
  36. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/date-all.ged +0 -0
  37. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/date-dual-invalid.ged +0 -0
  38. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/date-dual-valid.ged +0 -0
  39. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/enum-ext.ged +0 -0
  40. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/filename-1.ged +0 -0
  41. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/lang-all.ged +0 -0
  42. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/notes-1.ged +0 -0
  43. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/obje-1.ged +0 -0
  44. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/obsolete-1.ged +0 -0
  45. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/pedi-1.ged +0 -0
  46. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/rela_1.ged +0 -0
  47. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/sour-1.ged +0 -0
  48. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/tiny-1.ged +0 -0
  49. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/5/xref-case.ged +0 -0
  50. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/age-invalid.ged +0 -0
  51. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/age-valid.ged +0 -0
  52. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/atsign.ged +0 -0
  53. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/char_ascii_1.ged +0 -0
  54. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/char_ascii_2.ged +0 -0
  55. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/char_utf16be-1.ged +0 -0
  56. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/char_utf16be-2.ged +0 -0
  57. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/char_utf16le-1.ged +0 -0
  58. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/char_utf16le-2.ged +0 -0
  59. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/char_utf8-1.ged +0 -0
  60. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/char_utf8-2.ged +0 -0
  61. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/char_utf8-3.ged +0 -0
  62. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/date-all.ged +0 -0
  63. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/date-dual-invalid.ged +0 -0
  64. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/enum-ext.ged +0 -0
  65. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/filename-1.ged +0 -0
  66. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/lang-all.ged +0 -0
  67. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/notes-1.ged +0 -0
  68. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/obje-1.ged +0 -0
  69. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/obsolete-1.ged +0 -0
  70. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/pedi-1.ged +0 -0
  71. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/rela_1.ged +0 -0
  72. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/sour-1.ged +0 -0
  73. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/tiny-1.ged +0 -0
  74. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/7/xref-case.ged +0 -0
  75. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/LICENSE +0 -0
  76. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/gedcom7code/UPSTREAM-README.md +0 -0
  77. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/zip-interop/README.md +0 -0
  78. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/zip-interop/python-zip64.gdz +0 -0
  79. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/fixtures/vendored/zip-interop/python-zipfile.gdz +0 -0
  80. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/pyproject.toml +0 -0
  81. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/convert.rs +0 -0
  82. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/decode.rs +0 -0
  83. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/family.rs +0 -0
  84. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/gedzip.rs +0 -0
  85. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/lib.rs +0 -0
  86. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/names.rs +0 -0
  87. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/schema.rs +0 -0
  88. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/synthetic.rs +0 -0
  89. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/tags.rs +0 -0
  90. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/validate.rs +0 -0
  91. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/src/view.rs +0 -0
  92. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/tests/endurance.rs +0 -0
  93. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/tests/external_charset.rs +0 -0
  94. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/tests/properties.rs +0 -0
  95. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/tests/requirement_corpus.rs +0 -0
  96. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/tests/spec_conformance.rs +0 -0
  97. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/tests/typescript.rs +0 -0
  98. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/tests/vendored_corpus.rs +0 -0
  99. {gedcomkit-0.1.19 → gedcomkit-0.1.20}/tests/version_conversion.rs +0 -0
@@ -7,6 +7,18 @@ rules.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.20] - 2026-11-09
11
+
12
+ ### Fixed
13
+
14
+ - The date reader reads a calendar named after a qualifier or a range
15
+ word, as both grammars write it: `INT @#DJULIAN@ 2 MAR 1700 (…)`,
16
+ `BEF @#DJULIAN@ 2 JAN 1700`, `ABT JULIAN 1700`, `BET @#DJULIAN@ 1700 AND
17
+ @#DJULIAN@ 1710`. Such a date was left unparsed before, though
18
+ `to_gedcom_5_5_1` writes exactly this form, so what it wrote did not read
19
+ back. A range whose ends name different calendars, or whose unnamed end
20
+ (Gregorian) disagrees with a named one, stays unparsed.
21
+
10
22
  ## [0.1.19] - 2026-10-17
11
23
 
12
24
  ### Added
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.19"
38
+ version = "0.1.20"
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.19"
6
+ version = "0.1.20"
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.19
3
+ Version: 0.1.20
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -42,12 +42,8 @@ npm install gedcomkit
42
42
  2. Run the gates and release dry run:
43
43
 
44
44
  ```bash
45
- cargo fmt --all --check
46
- cargo clippy --all-features --all-targets -- -D warnings
47
- cargo test --all-features
48
- cargo test --no-default-features
49
- cargo check --manifest-path bindings/gedcomkit-wasm/Cargo.toml --target wasm32-unknown-unknown
50
- cargo check --manifest-path bindings/gedcomkit-py/Cargo.toml
45
+ cargo ci
46
+ cargo +1.88 ci test
51
47
  cargo publish --locked --dry-run
52
48
  bash scripts/check-release-tag.sh vX.Y.Z
53
49
  ```
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.19"
38
+ version = "0.1.20"
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.19"
46
+ version = "0.1.20"
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.19"
5
+ version = "0.1.20"
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"
@@ -316,7 +316,7 @@ impl Node {
316
316
  /// beside a space loses that space on re-joining. Failing that — a run
317
317
  /// of spaces wider than the limit, say — the cut falls back to the
318
318
  /// latest legal byte boundary instead, which is documented behavior of
319
- /// [`cut`] this call reuses, not a special case here.
319
+ /// the private `cut` this call reuses, not a special case here.
320
320
  ///
321
321
  /// The rendered line is measured in UTF-8 bytes, including level,
322
322
  /// optional cross-reference, tag, delimiters, and a terminator reserved
@@ -300,8 +300,19 @@ impl GedcomDate {
300
300
  pub fn parse(payload: &str) -> Self {
301
301
  let original = payload.to_owned();
302
302
  let (escape, rest) = split_calendar(payload.trim());
303
- let calendar = escape.unwrap_or_default();
304
303
  let (rest, phrase) = split_phrase(&rest);
304
+ // Both grammars put the calendar after a qualifier or a range word
305
+ // (`ABT @#DJULIAN@ 1700`, `BET JULIAN 1700 AND JULIAN 1710`): read
306
+ // there when the payload does not open with one, and only when every
307
+ // part of the payload names the same calendar.
308
+ let (escape, rest) = match escape {
309
+ Some(escape) => (Some(escape), rest),
310
+ None => match inner_calendar(&rest) {
311
+ Some((calendar, rest)) => (Some(calendar), rest),
312
+ None => (None, rest),
313
+ },
314
+ };
315
+ let calendar = escape.unwrap_or_default();
305
316
  let declared = escape.is_some();
306
317
  let dual = dual_year(&rest);
307
318
  let mut date = parse_expression(&rest)
@@ -690,7 +701,7 @@ fn parse_expression(input: &str) -> Option<GedcomDate> {
690
701
  // read (`FROM ABT 1924 TO 1959` puts a qualifier inside the range, which
691
702
  // neither version's grammar allows) would otherwise leave a range with
692
703
  // that end gone and nothing to say so.
693
- if let Some((left, right)) = split_keyword(trimmed, &["AND", "ET", "UND", "OG"])
704
+ if let Some((left, right)) = split_keyword(trimmed, &RANGE_AND)
694
705
  && let Some(prefix) =
695
706
  strip_keyword(left, &["BET", "BETWEEN", "ENTRE", "ZWISCHEN", "MELLOM"])
696
707
  {
@@ -698,7 +709,7 @@ fn parse_expression(input: &str) -> Option<GedcomDate> {
698
709
  let to = parse_point(right)?;
699
710
  return build(DateKind::Between, Some(from), Some(to));
700
711
  }
701
- if let Some((left, right)) = split_keyword(trimmed, &["TO", "AU", "BIS", "TIL"])
712
+ if let Some((left, right)) = split_keyword(trimmed, &RANGE_TO)
702
713
  && let Some(prefix) = strip_keyword(left, &["FROM", "DE", "DU", "VON", "FRA"])
703
714
  {
704
715
  let from = parse_point(prefix)?;
@@ -815,6 +826,81 @@ fn split_calendar(input: &str) -> (Option<Calendar>, String) {
815
826
  (None, input.to_owned())
816
827
  }
817
828
 
829
+ /// The words between a range's ends, in every spelling the reader takes:
830
+ /// `BET x AND y` and `FROM x TO y`.
831
+ const RANGE_AND: [&str; 4] = ["AND", "ET", "UND", "OG"];
832
+ const RANGE_TO: [&str; 4] = ["TO", "AU", "BIS", "TIL"];
833
+
834
+ /// The words a calendar may follow inside a date: a qualifier, or a word of
835
+ /// a range.
836
+ const BEFORE_CALENDAR: [&str; 10] = [
837
+ "ABT", "CAL", "EST", "BEF", "AFT", "INT", "BET", "AND", "FROM", "TO",
838
+ ];
839
+
840
+ /// The calendar named after a qualifier or a range word, and the payload
841
+ /// without it, when at least one part names one and every naming agrees.
842
+ /// A 5.5.1 escape (`@#DJULIAN@`, `@#DFRENCH R@`) or a GEDCOM 7 calendar word
843
+ /// (`JULIAN`) is read only directly after one of [`BEFORE_CALENDAR`], so
844
+ /// words elsewhere are left alone. Two different calendars, or a range
845
+ /// whose ends do not all name one, give `None`, and the payload stays as it
846
+ /// was for the reader to leave unparsed.
847
+ fn inner_calendar(input: &str) -> Option<(Calendar, String)> {
848
+ let mut found: Option<Calendar> = None;
849
+ let mut kept: Vec<&str> = Vec::new();
850
+ let mut rest = input.trim();
851
+ let mut after_keyword = false;
852
+ let mut namings = 0;
853
+ while !rest.is_empty() {
854
+ let named = if after_keyword {
855
+ if let Some(escape) = rest.strip_prefix("@#D")
856
+ && let Some((name, remainder)) = escape.split_once('@')
857
+ {
858
+ Some((calendar_from(name), remainder))
859
+ } else {
860
+ let (word, remainder) = rest.split_once(' ').unwrap_or((rest, ""));
861
+ matches!(
862
+ word,
863
+ "GREGORIAN" | "JULIAN" | "HEBREW" | "FRENCH_R" | "ROMAN"
864
+ )
865
+ .then(|| (calendar_from(word), remainder))
866
+ }
867
+ } else {
868
+ None
869
+ };
870
+ if let Some((calendar, remainder)) = named {
871
+ if found.is_some_and(|known| known != calendar) {
872
+ return None;
873
+ }
874
+ found = Some(calendar);
875
+ namings += 1;
876
+ rest = remainder.trim_start();
877
+ after_keyword = false;
878
+ continue;
879
+ }
880
+ let (word, remainder) = rest.split_once(' ').unwrap_or((rest, ""));
881
+ after_keyword = BEFORE_CALENDAR
882
+ .iter()
883
+ .any(|keyword| word.eq_ignore_ascii_case(keyword));
884
+ kept.push(word);
885
+ rest = remainder.trim_start();
886
+ }
887
+ // Each end of a range names its own calendar; one left unnamed is
888
+ // Gregorian, which one date cannot hold beside another calendar.
889
+ let ends = 1 + kept
890
+ .iter()
891
+ .enumerate()
892
+ .filter(|(at, word)| {
893
+ let is = |words: &[&str]| words.iter().any(|w| word.eq_ignore_ascii_case(w));
894
+ is(&RANGE_AND) || (*at > 0 && is(&RANGE_TO))
895
+ })
896
+ .count();
897
+ // An unnamed end is Gregorian, which agrees only with a Gregorian one.
898
+ if namings != ends && found != Some(Calendar::Gregorian) {
899
+ return None;
900
+ }
901
+ found.map(|calendar| (calendar, kept.join(" ")))
902
+ }
903
+
818
904
  /// The number of days in a month of a year, in the Julian calendar or the
819
905
  /// Gregorian. The year is counted as given, with no shift for the missing
820
906
  /// year zero; [`day_exists`] makes that shift before it asks.
@@ -2113,4 +2199,54 @@ mod tests {
2113
2199
  ["ABT 1875", "5 JAN 1882", "BET 1890 AND 1895", "1900"]
2114
2200
  );
2115
2201
  }
2202
+ #[test]
2203
+ fn a_calendar_after_a_qualifier_or_range_word_is_read() {
2204
+ let julian = |text: &str| {
2205
+ let date = GedcomDate::parse(text);
2206
+ (date.calendar, date.kind, date.earliest, date.phrase)
2207
+ };
2208
+ let (calendar, kind, earliest, phrase) = julian("INT @#DJULIAN@ 2 MAR 1700 (x)");
2209
+ assert_eq!(calendar, Calendar::Julian);
2210
+ assert_eq!(kind, DateKind::Interpreted);
2211
+ assert_eq!(
2212
+ earliest.map(|point| (point.month, point.day)),
2213
+ Some((Some(3), Some(2)))
2214
+ );
2215
+ assert_eq!(phrase.as_deref(), Some("x"));
2216
+ for text in [
2217
+ "BEF @#DJULIAN@ 2 JAN 1700",
2218
+ "ABT JULIAN 1700",
2219
+ "BET @#DJULIAN@ 1700 AND @#DJULIAN@ 1710",
2220
+ "FROM JULIAN 1700 TO JULIAN 1710",
2221
+ ] {
2222
+ let date = GedcomDate::parse(text);
2223
+ assert_eq!(date.calendar, Calendar::Julian, "{text}");
2224
+ assert!(date.is_parsed(), "{text}");
2225
+ assert_eq!(date.original, text);
2226
+ }
2227
+ assert_eq!(
2228
+ GedcomDate::parse("BEF @#DJULIAN@ 2 JAN 1700").display(),
2229
+ "before 2 January 1700"
2230
+ );
2231
+ // One end unnamed, or two calendars: not one date.
2232
+ for text in [
2233
+ "BET @#DJULIAN@ 1700 AND 1710",
2234
+ "BET @#DJULIAN@ 1700 ET 1710",
2235
+ "FROM @#DJULIAN@ 1700 BIS 1710",
2236
+ "BET @#DJULIAN@ 1700 AND @#DGREGORIAN@ 1710",
2237
+ ] {
2238
+ let date = GedcomDate::parse(text);
2239
+ assert!(!date.is_parsed(), "{text}");
2240
+ assert_eq!(date.calendar, Calendar::Gregorian, "{text}");
2241
+ }
2242
+ // An unnamed end is Gregorian, so it agrees with a Gregorian one.
2243
+ assert!(GedcomDate::parse("BET @#DGREGORIAN@ 1700 AND 1710").is_parsed());
2244
+ // A calendar word anywhere else is no calendar.
2245
+ assert!(!GedcomDate::parse("1700 JULIAN").is_parsed());
2246
+ // What the 5.5.1 writer writes, the reader reads back.
2247
+ let written = GedcomDate::parse("@#DJULIAN@ 1740/41").to_gedcom_5_5_1();
2248
+ let back = GedcomDate::parse(written.as_deref().unwrap_or_default());
2249
+ assert_eq!(back.calendar, Calendar::Julian);
2250
+ assert_eq!(back.earliest.map(|point| point.year), Some(1740));
2251
+ }
2116
2252
  }
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