From e12b092f07f109be67456a6298b6f329dce17df1 Mon Sep 17 00:00:00 2001 From: fan wu <336798967+FanWu-ai@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:25:44 -0700 Subject: [PATCH 1/2] Preserve exact fractional Unix timestamps in from_format Parse X and x tokens into integer microseconds before splitting normalized seconds and fractions. Avoid negative-fraction errors, scientific-notation failures and float precision loss. AI assistance: OpenAI Codex. --- docs/docs/instantiation.md | 11 +++++++++ src/pendulum/formatting/formatter.py | 21 ++++++++++------- tests/datetime/test_from_format.py | 35 ++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/docs/docs/instantiation.md b/docs/docs/instantiation.md index a49c6a26c..8f6d0fd77 100644 --- a/docs/docs/instantiation.md +++ b/docs/docs/instantiation.md @@ -119,6 +119,17 @@ It also accepts a `tz` keyword argument to specify the timezone: '1975-05-21T22:00:00+01:00' ``` +The `X` (seconds) and `x` (milliseconds) tokens also parse Unix timestamps, +including values before the Unix epoch. The `X` token accepts fractional seconds +with up to six decimal places: + +```python +>>> pendulum.from_format('0.000001', 'X').isoformat() +'1970-01-01T00:00:00.000001+00:00' +>>> pendulum.from_format('-1', 'x').isoformat() +'1969-12-31T23:59:59.999000+00:00' +``` + The final helper is for working with unix timestamps. `from_timestamp()` will create a `DateTime` instance equal to the given timestamp and will set the timezone as well or default it to `UTC`. diff --git a/src/pendulum/formatting/formatter.py b/src/pendulum/formatting/formatter.py index b09977d0d..bc92c5797 100644 --- a/src/pendulum/formatting/formatter.py +++ b/src/pendulum/formatting/formatter.py @@ -45,6 +45,15 @@ _MATCH_TIMEZONE = "[A-Za-z0-9-+]+(/[A-Za-z0-9-+_]+)?" +def _parse_timestamp(timestamp: str) -> int: + seconds, _, fraction = timestamp.partition(".") + microseconds = int(fraction.ljust(6, "0")) + if timestamp.startswith("-"): + microseconds = -microseconds + + return int(seconds) * 1_000_000 + microseconds + + class Formatter: _TOKENS: str = ( r"\[([^\[]*)\]|\\(.)|" @@ -235,8 +244,8 @@ class Formatter: "SSSSS": lambda us: int(us) * 10, "SSSSSS": lambda us: int(us), "a": lambda meridiem: meridiem, - "X": lambda ts: float(ts), - "x": lambda ts: float(ts) / 1e3, + "X": _parse_timestamp, + "x": lambda ts: int(ts) * 1000, "ZZ": str, "Z": str, "z": str, @@ -439,15 +448,11 @@ def _check_parsed( # If timestamp has been specified # we use it and don't go any further if parsed["timestamp"] is not None: - str_us = str(parsed["timestamp"]) - if "." in str_us: - microseconds = int(f"{str_us.split('.')[1].ljust(6, '0')}") - else: - microseconds = 0 + seconds, microseconds = divmod(parsed["timestamp"], 1_000_000) from pendulum.helpers import local_time - time = local_time(parsed["timestamp"], 0, microseconds) + time = local_time(seconds, 0, microseconds) validated["year"] = time[0] validated["month"] = time[1] validated["day"] = time[2] diff --git a/tests/datetime/test_from_format.py b/tests/datetime/test_from_format.py index ecf59632a..22c50edbc 100644 --- a/tests/datetime/test_from_format.py +++ b/tests/datetime/test_from_format.py @@ -14,6 +14,41 @@ def test_from_format_returns_datetime(): assert d.timezone_name == "UTC" +@pytest.mark.parametrize( + "text,fmt,expected", + [ + ("0.000001", "X", "1970-01-01T00:00:00.000001+00:00"), + ("0.00001", "X", "1970-01-01T00:00:00.000010+00:00"), + ("0.000012", "X", "1970-01-01T00:00:00.000012+00:00"), + ("-0.000001", "X", "1969-12-31T23:59:59.999999+00:00"), + ("-0.000012", "X", "1969-12-31T23:59:59.999988+00:00"), + ("-0.1", "X", "1969-12-31T23:59:59.900000+00:00"), + ("-1.2", "X", "1969-12-31T23:59:58.800000+00:00"), + ("-86400.25", "X", "1969-12-30T23:59:59.750000+00:00"), + ("-1", "x", "1969-12-31T23:59:59.999000+00:00"), + ("-1234", "x", "1969-12-31T23:59:58.766000+00:00"), + ("-86400250", "x", "1969-12-30T23:59:59.750000+00:00"), + ("0", "X", "1970-01-01T00:00:00+00:00"), + ("0", "x", "1970-01-01T00:00:00+00:00"), + ("-1", "X", "1969-12-31T23:59:59+00:00"), + ("-1000", "x", "1969-12-31T23:59:59+00:00"), + ("-0.5", "X", "1969-12-31T23:59:59.500000+00:00"), + ("1.123456", "X", "1970-01-01T00:00:01.123456+00:00"), + ("1", "x", "1970-01-01T00:00:00.001000+00:00"), + ("1234567890.123456", "X", "2009-02-13T23:31:30.123456+00:00"), + ("1234567890123", "x", "2009-02-13T23:31:30.123000+00:00"), + ("10000000000.123456", "X", "2286-11-20T17:46:40.123456+00:00"), + ("10000000000.12", "X", "2286-11-20T17:46:40.120000+00:00"), + ("-10000000000.123456", "X", "1653-02-10T06:13:19.876544+00:00"), + ("253402300799.999999", "X", "9999-12-31T23:59:59.999999+00:00"), + ("-62135596800", "X", "0001-01-01T00:00:00+00:00"), + ("+0.000012", "X", "1970-01-01T00:00:00.000012+00:00"), + ], +) +def test_from_format_fractional_timestamp(text: str, fmt: str, expected: str) -> None: + assert pendulum.from_format(text, fmt).isoformat() == expected + + def test_from_format_rejects_extra_text(): with pytest.raises(ValueError): pendulum.from_format("1975-05-21 22:32:11 extra text", "YYYY-MM-DD HH:mm:ss") From d0b04f305b2b538d0a92d6546c9b96debd524fcf Mon Sep 17 00:00:00 2001 From: fan wu <336798967+FanWu-ai@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:36:21 -0700 Subject: [PATCH 2/2] Avoid native debug overflow when converting timestamp calendar fields Use exact stdlib epoch arithmetic instead of the existing Rust local_time helper. Preserve every regression and documentation assertion, including year1 and year9999, and verify full debug-native and pure suites on Python3.10 and3.12. AI assistance: OpenAI Codex. --- src/pendulum/formatting/formatter.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/pendulum/formatting/formatter.py b/src/pendulum/formatting/formatter.py index bc92c5797..1953ef1b3 100644 --- a/src/pendulum/formatting/formatter.py +++ b/src/pendulum/formatting/formatter.py @@ -448,18 +448,16 @@ def _check_parsed( # If timestamp has been specified # we use it and don't go any further if parsed["timestamp"] is not None: - seconds, microseconds = divmod(parsed["timestamp"], 1_000_000) - - from pendulum.helpers import local_time - - time = local_time(seconds, 0, microseconds) - validated["year"] = time[0] - validated["month"] = time[1] - validated["day"] = time[2] - validated["hour"] = time[3] - validated["minute"] = time[4] - validated["second"] = time[5] - validated["microsecond"] = time[6] + time = datetime.datetime(1970, 1, 1) + datetime.timedelta( + microseconds=parsed["timestamp"] + ) + validated["year"] = time.year + validated["month"] = time.month + validated["day"] = time.day + validated["hour"] = time.hour + validated["minute"] = time.minute + validated["second"] = time.second + validated["microsecond"] = time.microsecond return validated