Skip to content
Open
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
11 changes: 11 additions & 0 deletions docs/docs/instantiation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
39 changes: 21 additions & 18 deletions src/pendulum/formatting/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"\[([^\[]*)\]|\\(.)|"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -439,22 +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:
str_us = str(parsed["timestamp"])
if "." in str_us:
microseconds = int(f"{str_us.split('.')[1].ljust(6, '0')}")
else:
microseconds = 0

from pendulum.helpers import local_time

time = local_time(parsed["timestamp"], 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

Expand Down
35 changes: 35 additions & 0 deletions tests/datetime/test_from_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading