Source code for pcapkit.utilities.datetime
# -*- coding: utf-8 -*-
"""Timestamp Conversion
==========================
.. module:: pcapkit.utilities.datetime
:mod:`pcapkit.utilities.datetime` converts a UNIX timestamp to an aware
:class:`~datetime.datetime` without the C library's range (:issue:`2099`), and
a :class:`~datetime.datetime` back to a UNIX timestamp without GraalPy's lost
second before 1970 (:issue:`2134`).
"""
import datetime
import math
import operator
import sys
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from datetime import tzinfo
__all__ = ['fromtimestamp', 'totimestamp']
# Avoids the platform ``gmtime()``, narrower than ``datetime``'s on Windows (#2099).
# ``fromtimestamp`` splits a float as CPython does (``math.modf``, the fraction
# times ``1e6`` rounded half to even, carried into ``[0, 1e6)``) on every
# interpreter, so every instant gets glibc ``gmtime_r``'s result. Its
# errors are CPython's on 64-bit Linux, except past a C ``int`` year (about 6.7e16
# seconds), where CPython raises ``OSError`` and this ``ValueError``. On GraalPy,
# a ``tz`` offset out of range raises ``ValueError`` from its ``fromutc`` (#2134).
_EPOCH = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
#: First and last whole second since the UNIX epoch in years 1 to 9999.
_EPOCH_MIN = -62_135_596_800
_EPOCH_MAX = 253_402_300_799
#: Microseconds per second, the resolution of :class:`~datetime.datetime`.
_MICROSECONDS = datetime.timedelta(seconds=1) // datetime.timedelta(microseconds=1)
#: A 64-bit ``time_t`` holds ``[-_TIME_T, _TIME_T)``.
_TIME_T = 2**63
# GraalPy 25.4.4 truncates an aware datetime's whole seconds toward zero, so
# 23:59:59.5 on 1969-12-31 UTC gives ``0.5``, not ``-0.5`` (#2134): its
# ``TimestampNode`` uses ``ChronoUnit.SECONDS.between`` (DateTimeBuiltins.java,
# no upstream issue filed). Elsewhere ``totimestamp`` is the stdlib call itself.
_TIMESTAMP_TRUNCATES = sys.implementation.name == 'graalpy'
[docs]
def fromtimestamp(timestamp: 'float | int', tz: 'tzinfo') -> 'datetime.datetime':
""":meth:`datetime.datetime.fromtimestamp` with a ``tz``, minus the C library's range.
The result equals ``datetime.datetime.fromtimestamp(timestamp, tz)`` on
64-bit Linux for every ``timestamp`` that call accepts, on every platform:
whole seconds and microseconds split from a float as CPython splits them,
rounding half to even, then added to the UNIX epoch and rendered in ``tz``.
The standard library breaks the instant down with the C library's
``gmtime()``, whose range on Windows is 1970 to 3000, so there it raises
:exc:`OSError` for instants :class:`~datetime.datetime` holds.
Args:
timestamp: Seconds since the UNIX epoch.
tz: Timezone the result is rendered in.
Returns:
The instant ``timestamp`` names, rendered in ``tz``.
Raises:
ValueError: If ``timestamp`` is NaN, or names a year outside 1 to 9999.
OverflowError: If ``timestamp`` is past a 64-bit ``time_t``, or ``tz``
moves the result out of the range :class:`~datetime.datetime` holds.
.. versionadded:: 1.5.0
Every capture and MH timestamp is converted with it (:issue:`2099`).
"""
if isinstance(timestamp, float):
if math.isnan(timestamp):
raise ValueError('Invalid value NaN (not a number)')
frac, whole = math.modf(timestamp)
# ``round`` of a float is half to even; the carry is -1, 0 or 1
carry, micro = divmod(round(frac * _MICROSECONDS), _MICROSECONDS)
whole += carry
else:
whole, micro = operator.index(timestamp), 0
if not -_TIME_T <= whole < _TIME_T:
raise OverflowError('timestamp out of range for platform time_t')
seconds = int(whole)
if not _EPOCH_MIN <= seconds <= _EPOCH_MAX:
raise ValueError(f'year is out of range for timestamp {timestamp!r}')
utc = _EPOCH + datetime.timedelta(seconds=seconds, microseconds=micro)
return tz.fromutc(utc.replace(tzinfo=tz))
[docs]
def totimestamp(dt: 'datetime.datetime') -> 'float':
""":meth:`datetime.datetime.timestamp`, as CPython computes it, on every interpreter.
For an aware ``dt`` that is ``(dt - epoch).total_seconds()``. GraalPy's own
counts the whole seconds toward zero, so there an instant before 1970 with a
fraction of a second comes out one second late, and this computes it as
CPython does. A naive ``dt`` is local time, and is ``dt.timestamp()``
everywhere.
Args:
dt: Moment to convert.
Returns:
Seconds since the UNIX epoch.
.. versionadded:: 1.5.0
The PCAP and PCAP-NG writers and the MH replay protection option convert
a :class:`~datetime.datetime` with it (:issue:`2134`).
"""
if _TIMESTAMP_TRUNCATES and dt.tzinfo is not None:
return (dt - _EPOCH).total_seconds()
return dt.timestamp()