Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion gedcom7/cast.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ def _cast_time(value: str) -> types.Time:
hour=int(match.group("hour")),
minute=int(match.group("minute")),
second=int(match.group("second")) if match.group("second") else None,
fraction=int(match.group("fraction")) if match.group("fraction") else None,
# kept as a string: see the note on Time.fraction
fraction=match.group("fraction"),
)


Expand Down
6 changes: 5 additions & 1 deletion gedcom7/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ class Time:
hour: int
minute: int
second: int | None = None
fraction: int | None = None
# The digits after the decimal point, kept verbatim. Leading zeros are part
# of the value: ".05" and ".5" are different instants. Trailing zeros are
# not, but they belong to the payload, so keeping the digits as written
# preserves those too.
fraction: str | None = None
tz: Literal["Z"] | None = None


Expand Down
17 changes: 15 additions & 2 deletions test/test_cast.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def test_cast_time() -> None:
hour=13,
minute=15,
second=12,
fraction=246,
fraction="246",
tz=None,
)
assert cast._cast_time("13:15Z") == types.Time(
Expand All @@ -102,13 +102,26 @@ def test_cast_time() -> None:
hour=13,
minute=15,
second=12,
fraction=246,
fraction="246",
tz="Z",
)
with pytest.raises(ValueError):
cast._cast_time("13:15A") # Invalid timezone, should be Z


def test_cast_time_preserves_fraction_width() -> None:
"""The fraction is kept verbatim, digits as written.

Leading zeros are part of the value, so ".05" and ".5" are different
instants. Trailing zeros do not change the value but do belong to the
payload, so they are preserved as well.
"""
assert cast._cast_time("13:15:12.05").fraction == "05"
assert cast._cast_time("13:15:12.5").fraction == "5"
assert cast._cast_time("13:15:12.500").fraction == "500"
assert cast._cast_time("13:15:12.05") != cast._cast_time("13:15:12.5")


def test_cast_age() -> None:
# Testing basic combinations
assert cast._cast_age("3y") == types.Age(
Expand Down
17 changes: 13 additions & 4 deletions test/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,20 +126,29 @@ def test_time_with_seconds() -> None:

def test_time_with_fraction() -> None:
"""Test time conversion with fractional seconds."""
gedcom_time = types.Time(hour=14, minute=30, second=45, fraction=123456)
gedcom_time = types.Time(hour=14, minute=30, second=45, fraction="123456")
python_time = util.time_to_python_time(gedcom_time)

assert python_time.second == 45
assert python_time.microsecond == 123456

# Test with different fraction lengths
gedcom_time = types.Time(hour=14, minute=30, second=45, fraction=5)
gedcom_time = types.Time(hour=14, minute=30, second=45, fraction="5")
python_time = util.time_to_python_time(gedcom_time)

assert python_time.second == 45
assert python_time.microsecond == 500000


def test_time_fraction_leading_zero_is_significant() -> None:
"""A leading zero scales the fraction: ".05" is 50 ms, not 500 ms."""
gedcom_time = types.Time(hour=14, minute=30, second=45, fraction="05")
assert util.time_to_python_time(gedcom_time).microsecond == 50000

gedcom_time = types.Time(hour=14, minute=30, second=45, fraction="5")
assert util.time_to_python_time(gedcom_time).microsecond == 500000


def test_time_without_fraction_or_seconds() -> None:
"""Test time conversion without fraction or seconds."""
gedcom_time = types.Time(hour=14, minute=30, second=None, fraction=None)
Expand Down Expand Up @@ -168,7 +177,7 @@ def test_date_only() -> None:
def test_date_and_time() -> None:
"""Test conversion with both date and time."""
gedcom_date = types.DateExact(day=15, month="JAN", year=2023)
gedcom_time = types.Time(hour=14, minute=30, second=45, fraction=123)
gedcom_time = types.Time(hour=14, minute=30, second=45, fraction="123")

python_datetime = util.date_exact_and_time_to_python_datetime(
gedcom_date, gedcom_time
Expand Down Expand Up @@ -197,7 +206,7 @@ def test_integration_with_other_functions() -> None:
)

gedcom_date = types.DateExact(day=15, month="JAN", year=2023)
gedcom_time = types.Time(hour=14, minute=30, second=45, fraction=123)
gedcom_time = types.Time(hour=14, minute=30, second=45, fraction="123")

util.date_exact_and_time_to_python_datetime(gedcom_date, gedcom_time)

Expand Down