Skip to content

Commit fe032fb

Browse files
committed
Properly handle date values
1 parent a0f57ad commit fe032fb

File tree

3 files changed

+225
-2
lines changed

3 files changed

+225
-2
lines changed

gedcom7/cast.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,30 @@ def _cast_date(value: str) -> types.Date:
142142
)
143143

144144

145+
def _cast_date_approx(value: str) -> types.DateApprox:
146+
"""Cast a string to a DateApprox."""
147+
match = _match(value, grammar.dateapprox, "DateApprox")
148+
return types.DateApprox(
149+
date=_cast_date(match.group("dateapprox")),
150+
approx=match.group("qualifier"),
151+
)
152+
153+
154+
def _cast_date_range(value: str) -> types.DateRange:
155+
"""Cast a string to a DateRange."""
156+
match = _match(value, grammar.daterange, "DateRange")
157+
res = {}
158+
if match.group("between"):
159+
res["start"] = _cast_date(match.group("between"))
160+
if match.group("and"):
161+
res["end"] = _cast_date(match.group("and"))
162+
if match.group("after"):
163+
res["start"] = _cast_date(match.group("after"))
164+
if match.group("before"):
165+
res["end"] = _cast_date(match.group("before"))
166+
return types.DateRange(**res)
167+
168+
145169
def _cast_date_period(value: str) -> types.DatePeriod:
146170
"""Cast a string to a DatePeriod."""
147171
match = _match(value, grammar.dateperiod, "DatePeriod")
@@ -178,14 +202,35 @@ def _cast_date_period(value: str) -> types.DatePeriod:
178202
return types.DatePeriod(**res)
179203

180204

205+
def _cast_date_value(value: str) -> types.DateValue:
206+
"""Cast a string to a DateValue.
207+
208+
A DateValue can be one of the following:
209+
- A standard date: "1 OCT 2023"
210+
- A dateperiod: "FROM 1 JAN 2023 TO 31 DEC 2023"
211+
- A daterange: "BET 1 JAN 2023 AND 31 DEC 2023"
212+
- A dateapprox: "ABT 1 OCT 2023"
213+
"""
214+
dateapprox_match = re.fullmatch(grammar.dateapprox, value)
215+
if dateapprox_match:
216+
return _cast_date_approx(value)
217+
daterange_match = re.fullmatch(grammar.daterange, value)
218+
if daterange_match:
219+
return _cast_date_range(value)
220+
dateperiod_match = re.fullmatch(grammar.dateperiod, value)
221+
if dateperiod_match:
222+
return _cast_date_period(value)
223+
return _cast_date(value)
224+
225+
181226
CAST_FUNCTIONS: dict[str, Callable[[str], types.DataType] | None] = {
182227
"Y|<NULL>": _cast_bool,
183228
"http://www.w3.org/2001/XMLSchema#Language": None,
184229
"http://www.w3.org/2001/XMLSchema#nonNegativeInteger": _cast_integer,
185230
"http://www.w3.org/2001/XMLSchema#string": None,
186231
"http://www.w3.org/ns/dcat#mediaType": _cast_mediatype,
187232
"https://gedcom.io/terms/v7/type-Age": _cast_age,
188-
"https://gedcom.io/terms/v7/type-Date": _cast_date,
233+
"https://gedcom.io/terms/v7/type-Date": _cast_date_value,
189234
"https://gedcom.io/terms/v7/type-Date#exact": _cast_date_exact,
190235
"https://gedcom.io/terms/v7/type-Date#period": _cast_date_period,
191236
"https://gedcom.io/terms/v7/type-Enum": _cast_enum,

gedcom7/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ class DateApprox:
126126
class DateRange:
127127
"""Date range type."""
128128

129-
start: Date
129+
start: Date | None = None
130130
end: Date | None = None
131131

132132

test/test_cast.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,3 +336,181 @@ def test_cast_date_period():
336336
)
337337
with pytest.raises(ValueError):
338338
cast._cast_date_period("11 JAN 2022")
339+
340+
341+
def test_cast_date_range():
342+
assert cast._cast_date_range("BET 1 JAN 2020 AND 31 DEC 2020") == types.DateRange(
343+
start=types.Date(
344+
calendar=None,
345+
day=1,
346+
month="JAN",
347+
year=2020,
348+
epoch=None,
349+
),
350+
end=types.Date(
351+
calendar=None,
352+
day=31,
353+
month="DEC",
354+
year=2020,
355+
epoch=None,
356+
),
357+
)
358+
assert cast._cast_date_range("AFT 1 JAN 2020") == types.DateRange(
359+
start=types.Date(
360+
calendar=None,
361+
day=1,
362+
month="JAN",
363+
year=2020,
364+
epoch=None,
365+
),
366+
end=None,
367+
)
368+
369+
assert cast._cast_date_range("BEF 31 DEC 2020") == types.DateRange(
370+
start=None,
371+
end=types.Date(
372+
calendar=None,
373+
day=31,
374+
month="DEC",
375+
year=2020,
376+
epoch=None,
377+
),
378+
)
379+
380+
assert cast._cast_date_range(
381+
"BET JULIAN 1 FEB 1600 BCE AND 31 MAR 1500"
382+
) == types.DateRange(
383+
start=types.Date(
384+
calendar="JULIAN",
385+
day=1,
386+
month="FEB",
387+
year=1600,
388+
epoch="BCE",
389+
),
390+
end=types.Date(
391+
calendar=None,
392+
day=31,
393+
month="MAR",
394+
year=1500,
395+
epoch=None,
396+
),
397+
)
398+
399+
with pytest.raises(ValueError):
400+
cast._cast_date_range("FROM 1 JAN 2020 TO 31 DEC 2020")
401+
402+
with pytest.raises(ValueError):
403+
cast._cast_date_range("11 JAN 2022")
404+
405+
406+
def test_cast_date_approx():
407+
assert cast._cast_date_approx("ABT 11 JAN 2022") == types.DateApprox(
408+
date=types.Date(
409+
calendar=None,
410+
day=11,
411+
month="JAN",
412+
year=2022,
413+
epoch=None,
414+
),
415+
approx="ABT",
416+
)
417+
418+
assert cast._cast_date_approx("EST JULIAN 5 MAY 1755 BCE") == types.DateApprox(
419+
date=types.Date(
420+
calendar="JULIAN",
421+
day=5,
422+
month="MAY",
423+
year=1755,
424+
epoch="BCE",
425+
),
426+
approx="EST",
427+
)
428+
429+
assert cast._cast_date_approx("CAL 1 JAN 2021") == types.DateApprox(
430+
date=types.Date(
431+
calendar=None,
432+
day=1,
433+
month="JAN",
434+
year=2021,
435+
epoch=None,
436+
),
437+
approx="CAL",
438+
)
439+
440+
with pytest.raises(ValueError):
441+
cast._cast_date_approx("APPROXIMATELY 11 JAN 2022")
442+
443+
with pytest.raises(ValueError):
444+
cast._cast_date_approx("11 JAN 2022")
445+
446+
447+
def test_cast_date_value():
448+
# Test with standard date
449+
assert isinstance(cast._cast_date_value("11 JAN 2022"), types.Date)
450+
assert cast._cast_date_value("11 JAN 2022") == types.Date(
451+
calendar=None,
452+
day=11,
453+
month="JAN",
454+
year=2022,
455+
epoch=None,
456+
)
457+
458+
# Test with date approximation
459+
assert isinstance(cast._cast_date_value("ABT 11 JAN 2022"), types.DateApprox)
460+
assert cast._cast_date_value("ABT 11 JAN 2022") == types.DateApprox(
461+
date=types.Date(
462+
calendar=None,
463+
day=11,
464+
month="JAN",
465+
year=2022,
466+
epoch=None,
467+
),
468+
approx="ABT",
469+
)
470+
471+
# Test with date range
472+
assert isinstance(
473+
cast._cast_date_value("BET 1 JAN 2020 AND 31 DEC 2020"), types.DateRange
474+
)
475+
assert cast._cast_date_value("BET 1 JAN 2020 AND 31 DEC 2020") == types.DateRange(
476+
start=types.Date(
477+
calendar=None,
478+
day=1,
479+
month="JAN",
480+
year=2020,
481+
epoch=None,
482+
),
483+
end=types.Date(
484+
calendar=None,
485+
day=31,
486+
month="DEC",
487+
year=2020,
488+
epoch=None,
489+
),
490+
)
491+
492+
# Test with date period
493+
assert isinstance(
494+
cast._cast_date_value("FROM 1 JAN 2020 TO 31 DEC 2020"), types.DatePeriod
495+
)
496+
assert cast._cast_date_value("FROM 1 JAN 2020 TO 31 DEC 2020") == types.DatePeriod(
497+
from_=types.Date(
498+
calendar=None,
499+
day=1,
500+
month="JAN",
501+
year=2020,
502+
epoch=None,
503+
),
504+
to=types.Date(
505+
calendar=None,
506+
day=31,
507+
month="DEC",
508+
year=2020,
509+
epoch=None,
510+
),
511+
)
512+
513+
# Test with invalid format
514+
with pytest.raises(ValueError):
515+
cast._cast_date_value("2022-01-11")
516+

0 commit comments

Comments
 (0)